Deploy HF RL Explorer
Browse files- .gitignore +9 -0
- DESIGN.md +7 -0
- Dockerfile +1 -1
- app/admin.py +0 -379
- app/admin_app.py +0 -79
- app/settings.py +2 -36
- app/version.py +1 -1
- scripts/check_judge_relay.py +0 -57
- scripts/deploy_spaces.py +0 -115
- scripts/schedule_indexer.py +0 -146
- tests/conftest.py +0 -9
- tests/reward-stats.test.mjs +0 -20
- tests/schemaform.test.mjs +0 -89
- tests/space-media.test.mjs +0 -24
- tests/test_admin_dashboard.py +0 -94
- tests/test_agents_install.py +0 -60
- tests/test_audit_regressions.py +0 -163
- tests/test_auto_index.py +0 -86
- tests/test_capabilities.py +0 -68
- tests/test_community.py +0 -96
- tests/test_contract.py +0 -175
- tests/test_deploy_spaces.py +0 -12
- tests/test_discovery.py +0 -69
- tests/test_envs.py +0 -307
- tests/test_frameworks.py +0 -420
- tests/test_harbor_spec.py +0 -340
- tests/test_judge.py +0 -99
- tests/test_live.py +0 -409
- tests/test_mimo.py +0 -187
- tests/test_model_relay.py +0 -23
- tests/test_nemo_runner.py +0 -73
- tests/test_remote_mcp.py +0 -62
- tests/test_search_api.py +0 -629
- tests/test_security.py +0 -554
- tests/test_seo.py +0 -118
- tests/test_snapshot.py +0 -456
- tests/test_space_checks.py +0 -135
- tests/test_space_files.py +0 -449
- tests/test_space_tasks.py +0 -41
- tests/ui-audit.mjs +0 -320
- web-admin/admin.css +0 -175
- web-admin/admin.js +0 -406
- web-admin/index.html +0 -41
- web-admin/main.js +0 -28
.gitignore
CHANGED
|
@@ -3,3 +3,12 @@ __pycache__/
|
|
| 3 |
.venv/
|
| 4 |
.gradio/
|
| 5 |
.ruff_cache/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
.venv/
|
| 4 |
.gradio/
|
| 5 |
.ruff_cache/
|
| 6 |
+
|
| 7 |
+
# Admin implementation is maintained outside this public repository.
|
| 8 |
+
web-admin/
|
| 9 |
+
app/admin.py
|
| 10 |
+
app/admin_app.py
|
| 11 |
+
app/admin_access.py
|
| 12 |
+
tests/test_admin*.py
|
| 13 |
+
ADMIN_SPACE_README.md
|
| 14 |
+
EXPLORER_AUDIT.md
|
DESIGN.md
CHANGED
|
@@ -235,3 +235,10 @@ HF scheduled Job, hourly (scripts/schedule_indexer.py) Explorer / admin
|
|
| 235 |
5. **Smoke tests on the Space**: `/readyz`; `scripts/check_judge_relay.py`'s path through the Space's `/capture`
|
| 236 |
(a model-graded rollout, e.g. a MiMo General twin with Inkling as judge: reward recorded, `judge_calls > 0`);
|
| 237 |
`node tests/ui-audit.mjs https://<space>`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
5. **Smoke tests on the Space**: `/readyz`; `scripts/check_judge_relay.py`'s path through the Space's `/capture`
|
| 236 |
(a model-graded rollout, e.g. a MiMo General twin with Inkling as judge: reward recorded, `judge_calls > 0`);
|
| 237 |
`node tests/ui-audit.mjs https://<space>`.
|
| 238 |
+
|
| 239 |
+
## Admin source boundary
|
| 240 |
+
|
| 241 |
+
This public tooling directory contains only the Explorer. The dashboard application,
|
| 242 |
+
its assets, membership checks, tests and deployment tooling are maintained separately
|
| 243 |
+
in the private `FineEnvs/RL-Explorer-admin` Space. The Explorer reads shared settings
|
| 244 |
+
and moderation requests from the data bucket without importing the admin app.
|
Dockerfile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# HF RL Explorer
|
| 2 |
# /data the private bucket FineEnvs/rl-explorer-data: indexes, packs, rollouts, admin settings; mounted by both
|
| 3 |
# /tmp/... dataset files read while indexing and the tasks rollouts run on: many small files, local disk
|
| 4 |
# abcmidi the MiMo release's music scorer renders a piece with abc2midi (app/mimo/vendor/music_scorer)
|
|
|
|
| 1 |
+
# Public HF RL Explorer Docker Space. The admin has a separate private deployment.
|
| 2 |
# /data the private bucket FineEnvs/rl-explorer-data: indexes, packs, rollouts, admin settings; mounted by both
|
| 3 |
# /tmp/... dataset files read while indexing and the tasks rollouts run on: many small files, local disk
|
| 4 |
# abcmidi the MiMo release's music scorer renders a piece with abc2midi (app/mimo/vendor/music_scorer)
|
app/admin.py
DELETED
|
@@ -1,379 +0,0 @@
|
|
| 1 |
-
"""The admin dashboard's API, served by its own app (app/admin_app.py) on its own private Space, for members of the
|
| 2 |
-
admin organization only (see app/settings.py). Every route checks the signed-in user against the organization on
|
| 3 |
-
every request. Every change is written to the audit log with who made it.
|
| 4 |
-
|
| 5 |
-
The admin app shares the explorer's bucket but not its process. It reads the run records the explorer writes, and
|
| 6 |
-
what it changes goes into the shared settings file, which the explorer re-reads: collections, pins, hidden
|
| 7 |
-
environments, the rollout switches, rollouts taken off Community, and requests to stop a running rollout.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import gzip
|
| 13 |
-
import json
|
| 14 |
-
import re
|
| 15 |
-
import time
|
| 16 |
-
from collections import Counter
|
| 17 |
-
from typing import Any, Literal
|
| 18 |
-
|
| 19 |
-
from fastapi import APIRouter, HTTPException, Request
|
| 20 |
-
from pydantic import BaseModel, Field
|
| 21 |
-
|
| 22 |
-
from . import auth, auto_index, catalog, community, config, runner, search_api, settings, snapshot, space_checks, space_tasks, store
|
| 23 |
-
|
| 24 |
-
STALE = 600 # a rollout whose record hasn't changed for this long is no longer live, whatever it says
|
| 25 |
-
|
| 26 |
-
router = APIRouter(prefix="/api/admin")
|
| 27 |
-
KEY = re.compile(r"^(space:)?[A-Za-z0-9][\w.-]*/[\w.-]+$")
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def _admin(request: Request) -> dict[str, Any]:
|
| 31 |
-
u = auth.current_user(request)
|
| 32 |
-
if not u:
|
| 33 |
-
raise HTTPException(401, "sign in")
|
| 34 |
-
if not settings.is_admin(u):
|
| 35 |
-
raise HTTPException(403, f"only members of {settings.ADMIN_ORG} can open the admin dashboard")
|
| 36 |
-
return u
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
# ── overview ─────────────────────────────────────────────────────────────────
|
| 40 |
-
@router.get("/overview")
|
| 41 |
-
def overview(request: Request):
|
| 42 |
-
_admin(request)
|
| 43 |
-
envs = search_api.environments(include_hidden=True)
|
| 44 |
-
runs = _runs()
|
| 45 |
-
now = time.time()
|
| 46 |
-
day = 86400
|
| 47 |
-
by_day = Counter(time.strftime("%Y-%m-%d", time.gmtime(r.get("created_at", 0))) for r in runs if now - r.get("created_at", 0) < 14 * day)
|
| 48 |
-
finished = [r for r in runs if r.get("status") in ("done", "failed", "cancelled", "interrupted")]
|
| 49 |
-
new = sorted((e for e in envs if e.get("created") and now - _ts(e["created"]) < 14 * day), key=lambda e: e["created"], reverse=True)
|
| 50 |
-
hidden_runs = set(settings.get("hidden_runs", []))
|
| 51 |
-
ctx = search_api.context()
|
| 52 |
-
with snapshot.use() as (_, conn):
|
| 53 |
-
public_counts = search_api.sql_extra(conn, ctx, 0, search_api.Query(scope="ready"))["stats"]
|
| 54 |
-
idx_files = list(config.INDEX_DIR.glob("*.json.gz"))
|
| 55 |
-
pack_files = list(config.PACK_DIR.glob("*.json.gz")) if config.PACK_DIR.is_dir() else []
|
| 56 |
-
return {
|
| 57 |
-
"environments": {"datasets": sum(1 for e in envs if e["kind"] == "dataset"), "spaces": sum(1 for e in envs if e["kind"] == "space"),
|
| 58 |
-
"openenv": sum(space_checks.verified(e.get("api_check")) for e in envs), "indexed": sum(1 for e in envs if e.get("indexed")),
|
| 59 |
-
"checked_spaces": sum(space_checks.browseable(e.get("api_check")) for e in envs),
|
| 60 |
-
"public": public_counts, "hidden": len(catalog.hidden()), "pinned": len(catalog.pinned()), "collections": len(catalog.collections())},
|
| 61 |
-
"rollouts": {"total": len(runs), "live": sum(1 for r in runs if _live(r)), "today": sum(1 for r in runs if now - r.get("created_at", 0) < day),
|
| 62 |
-
"week": sum(1 for r in runs if now - r.get("created_at", 0) < 7 * day),
|
| 63 |
-
"public": sum(community.is_public(r, hidden_runs) for r in runs),
|
| 64 |
-
"private": sum(r.get("visibility") != "public" for r in runs),
|
| 65 |
-
"by_runner": Counter(community.runner(r) for r in runs).most_common(),
|
| 66 |
-
"failed": sum(1 for r in finished if r.get("status") != "done"), "finished": len(finished),
|
| 67 |
-
"users": len({r.get("user") for r in runs}),
|
| 68 |
-
"by_day": [[time.strftime("%Y-%m-%d", time.gmtime(now - i * day)), by_day.get(time.strftime("%Y-%m-%d", time.gmtime(now - i * day)), 0)]
|
| 69 |
-
for i in range(13, -1, -1)],
|
| 70 |
-
"by_model": Counter(r.get("model") or "custom endpoint" for r in runs).most_common(8),
|
| 71 |
-
"by_status": Counter(r.get("status") for r in runs).most_common()},
|
| 72 |
-
"storage": {"indexes": len(idx_files), "index_bytes": sum(f.stat().st_size for f in idx_files),
|
| 73 |
-
"packs": len(pack_files), "pack_bytes": sum(f.stat().st_size for f in pack_files)},
|
| 74 |
-
"recent": [_admin_view(r) for r in runs[:8]],
|
| 75 |
-
"live": [_admin_view(r) for r in runs if _live(r)][:24],
|
| 76 |
-
"operations": _operations(envs),
|
| 77 |
-
"new": new[:12], "new_count": len(new),
|
| 78 |
-
"settings": _settings(),
|
| 79 |
-
}
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def _ts(iso: str) -> float:
|
| 83 |
-
from datetime import datetime
|
| 84 |
-
|
| 85 |
-
try:
|
| 86 |
-
return datetime.fromisoformat(iso.replace("Z", "+00:00")).timestamp()
|
| 87 |
-
except ValueError:
|
| 88 |
-
return 0.0
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
_avatars: dict[str, tuple[float, str | None]] = {}
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
def avatar(name: str | None) -> str | None:
|
| 95 |
-
"""A Hub user's profile picture, looked up once a day (rollouts started before avatars were recorded have none)."""
|
| 96 |
-
if not name:
|
| 97 |
-
return None
|
| 98 |
-
hit = _avatars.get(name)
|
| 99 |
-
if hit and time.time() - hit[0] < 86400:
|
| 100 |
-
return hit[1]
|
| 101 |
-
import httpx
|
| 102 |
-
|
| 103 |
-
url = None
|
| 104 |
-
try:
|
| 105 |
-
r = httpx.get(f"https://huggingface.co/api/users/{name}/overview", timeout=6)
|
| 106 |
-
if r.status_code == 200:
|
| 107 |
-
url = r.json().get("avatarUrl")
|
| 108 |
-
url = url if isinstance(url, str) and url.startswith("https://") else None
|
| 109 |
-
except Exception: # noqa: BLE001
|
| 110 |
-
pass
|
| 111 |
-
_avatars[name] = (time.time(), url)
|
| 112 |
-
return url
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
def _live(r: dict[str, Any]) -> bool:
|
| 116 |
-
return r.get("status") in store.ACTIVE and time.time() - (r.get("updated_at") or 0) < STALE
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
def _runs() -> list[dict[str, Any]]:
|
| 120 |
-
store.refresh(10) # the explorer writes these; re-read them every few seconds
|
| 121 |
-
return store.list_runs(limit=100000)
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
def _admin_view(r: dict[str, Any]) -> dict[str, Any]:
|
| 125 |
-
"""A rollout for admins: everything but where a visitor's own endpoint lives."""
|
| 126 |
-
v = dict(r)
|
| 127 |
-
if v.get("endpoint"):
|
| 128 |
-
v["endpoint"] = {"host": v["endpoint"].get("host"), "model": v["endpoint"].get("model")}
|
| 129 |
-
v["runner"] = community.runner(r)
|
| 130 |
-
v["community"] = community.is_public(r, set(settings.get("hidden_runs", [])))
|
| 131 |
-
v["live"] = _live(r)
|
| 132 |
-
v["avatar"] = r.get("avatar") or avatar(r.get("user"))
|
| 133 |
-
v["moderated"] = r["id"] in set(settings.get("hidden_runs", []))
|
| 134 |
-
return v
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
# ── rollouts ─────────────────────────────────────────────────────────────────
|
| 138 |
-
@router.get("/rollouts")
|
| 139 |
-
def rollouts(request: Request, status: str = "", q: str = "", user: str = "", runner: str = "",
|
| 140 |
-
visibility: str = "", limit: int = 100, offset: int = 0):
|
| 141 |
-
_admin(request)
|
| 142 |
-
ql = q.strip().lower()
|
| 143 |
-
hidden = set(settings.get("hidden_runs", []))
|
| 144 |
-
all_runs = _runs()
|
| 145 |
-
runs = [r for r in all_runs
|
| 146 |
-
if (not status or (status == "live" and _live(r)) or r.get("status") == status)
|
| 147 |
-
and (not user or r.get("user") == user)
|
| 148 |
-
and (not runner or community.runner(r) == runner)
|
| 149 |
-
and (not visibility or (visibility == "community" and community.is_public(r, hidden))
|
| 150 |
-
or (visibility == "moderated" and r["id"] in hidden) or r.get("visibility") == visibility)
|
| 151 |
-
and (not ql or ql in f"{r.get('title', '')} {r.get('dataset', '')} {r.get('model', '')} {r.get('user', '')} {r['id']} {community.runner(r)}".lower())]
|
| 152 |
-
offset = max(0, offset)
|
| 153 |
-
return {"total": len(runs), "runners": sorted({community.runner(r) for r in all_runs}),
|
| 154 |
-
"runs": [_admin_view(r) for r in runs[offset:offset + max(1, min(limit, 500))]]}
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
@router.post("/rollouts/{run_id}/cancel")
|
| 158 |
-
def cancel(run_id: str, request: Request):
|
| 159 |
-
"""Ask the explorer to stop a running rollout: it watches the settings for these, and stops it within seconds."""
|
| 160 |
-
u = _admin(request)
|
| 161 |
-
r = store.get(run_id)
|
| 162 |
-
if not r:
|
| 163 |
-
raise HTTPException(404, "no such rollout")
|
| 164 |
-
asks = {k: v for k, v in settings.get("cancel_requests", {}).items() if time.time() - v < 3600}
|
| 165 |
-
asks[run_id] = time.time()
|
| 166 |
-
settings.save(u["name"], {"cancel_requests": asks}, note=f"asked to stop rollout {run_id}")
|
| 167 |
-
return {"cancelled": _live(r)}
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
class Moderate(BaseModel):
|
| 171 |
-
hidden: bool
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
@router.post("/rollouts/{run_id}/moderate")
|
| 175 |
-
def moderate(run_id: str, body: Moderate, request: Request):
|
| 176 |
-
"""Take a public rollout off Community (or put it back). Its owner still sees it."""
|
| 177 |
-
u = _admin(request)
|
| 178 |
-
r = store.get(run_id)
|
| 179 |
-
if not r:
|
| 180 |
-
raise HTTPException(404, "no such rollout")
|
| 181 |
-
gone = set(settings.get("hidden_runs", []))
|
| 182 |
-
gone = (gone | {run_id}) if body.hidden else (gone - {run_id})
|
| 183 |
-
settings.save(u["name"], {"hidden_runs": sorted(gone)}, note=f"{'hid' if body.hidden else 'restored'} rollout {run_id} on Community")
|
| 184 |
-
return {"run": _admin_view(r)}
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
# ── people ───────────────────────────────────────────────────────────────────
|
| 188 |
-
@router.get("/people")
|
| 189 |
-
def people(request: Request):
|
| 190 |
-
"""Everyone who has run a rollout: how much, how well, on what, and when last."""
|
| 191 |
-
_admin(request)
|
| 192 |
-
by: dict[str, dict[str, Any]] = {}
|
| 193 |
-
for r in _runs():
|
| 194 |
-
name = r.get("user") or "unknown"
|
| 195 |
-
p = by.setdefault(name, {"name": name, "avatar": None, "runs": 0, "done": 0, "scored": 0, "live": 0, "public": 0,
|
| 196 |
-
"first": r.get("created_at"), "last": r.get("created_at"), "models": Counter(), "datasets": Counter()})
|
| 197 |
-
p["avatar"] = p["avatar"] or r.get("avatar")
|
| 198 |
-
p["runs"] += 1
|
| 199 |
-
p["done"] += r.get("status") == "done"
|
| 200 |
-
p["scored"] += r.get("status") == "done" and community.scalar_reward(r)
|
| 201 |
-
p["live"] += _live(r)
|
| 202 |
-
p["public"] += community.is_public(r, set(settings.get("hidden_runs", [])))
|
| 203 |
-
p["first"] = min(p["first"] or 0, r.get("created_at") or 0)
|
| 204 |
-
p["last"] = max(p["last"] or 0, r.get("created_at") or 0)
|
| 205 |
-
p["models"][r.get("model") or "custom endpoint"] += 1
|
| 206 |
-
p["datasets"][r.get("dataset")] += 1
|
| 207 |
-
out = []
|
| 208 |
-
for p in sorted(by.values(), key=lambda x: -(x["last"] or 0)):
|
| 209 |
-
p["avatar"] = p["avatar"] or avatar(p["name"])
|
| 210 |
-
p["models"], p["datasets"] = p["models"].most_common(3), p["datasets"].most_common(3)
|
| 211 |
-
out.append(p)
|
| 212 |
-
return {"people": out, "me": auth.public(auth.current_user(request))}
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
# ── environments: pin, hide, feature, index ──────────────────────────────────
|
| 216 |
-
@router.get("/environments")
|
| 217 |
-
def environments(request: Request):
|
| 218 |
-
_admin(request)
|
| 219 |
-
envs = search_api.environments(include_hidden=True)
|
| 220 |
-
gone = catalog.hidden()
|
| 221 |
-
runs = Counter(r.get("dataset") for r in _runs())
|
| 222 |
-
return {"environments": [{**e, "hidden": e["key"] in gone, "ready": e["kind"] == "dataset" or space_checks.browseable(e.get("api_check")), "rollouts": runs.get(e["id"], 0) if e["kind"] == "dataset" else 0} for e in envs],
|
| 223 |
-
"collections": catalog.collections(), "jobs": _jobs()}
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
def _jobs() -> dict[str, Any]:
|
| 227 |
-
with catalog._jobs_lock:
|
| 228 |
-
return {k: {kk: vv for kk, vv in v.items() if kk not in ("sha",)} for k, v in catalog._jobs.items()
|
| 229 |
-
if v.get("state") not in ("done",)}
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
class EnvAction(BaseModel):
|
| 233 |
-
key: str = Field(max_length=220)
|
| 234 |
-
action: Literal["pin", "unpin", "hide", "unhide", "feature", "unfeature", "index"]
|
| 235 |
-
collection: str | None = Field(None, max_length=40)
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
@router.post("/environments")
|
| 239 |
-
def env_action(body: EnvAction, request: Request):
|
| 240 |
-
u = _admin(request)
|
| 241 |
-
if not KEY.match(body.key):
|
| 242 |
-
raise HTTPException(400, "not an environment key")
|
| 243 |
-
key = body.key
|
| 244 |
-
if body.action in ("pin", "unpin"):
|
| 245 |
-
pins = [k for k in catalog.pinned() if k != key] + ([key] if body.action == "pin" else [])
|
| 246 |
-
settings.save(u["name"], {"pinned": pins}, note=f"{body.action} {key}")
|
| 247 |
-
elif body.action in ("hide", "unhide"):
|
| 248 |
-
gone = sorted((catalog.hidden() - {key}) | ({key} if body.action == "hide" else set()))
|
| 249 |
-
settings.save(u["name"], {"hidden": gone}, note=f"{body.action} {key}")
|
| 250 |
-
elif body.action in ("feature", "unfeature"):
|
| 251 |
-
cols = [dict(c, ids=[k for k in c["ids"] if k != key]) for c in catalog.collections()]
|
| 252 |
-
if body.action == "feature":
|
| 253 |
-
target = next((c for c in cols if c["id"] == body.collection), None)
|
| 254 |
-
if not target:
|
| 255 |
-
raise HTTPException(400, "no such collection")
|
| 256 |
-
target["ids"].append(key)
|
| 257 |
-
settings.save(u["name"], {"collections": cols}, note=f"{body.action} {key}" + (f" in {body.collection}" if body.collection else ""))
|
| 258 |
-
elif body.action == "index":
|
| 259 |
-
if key.startswith("space:"):
|
| 260 |
-
raise HTTPException(400, "Space task counts are discovered automatically from their Task API; dataset indexes are separate")
|
| 261 |
-
st = catalog.index_status(key) # anonymous: public datasets only
|
| 262 |
-
settings.save(u["name"], {}, note=f"indexed {key}")
|
| 263 |
-
return {"ok": True, "job": {k: v for k, v in st.items() if k != "index"}}
|
| 264 |
-
return {"ok": True}
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
# ── collections ──────────────────────────────────────────────────────────────
|
| 268 |
-
class Collection(BaseModel):
|
| 269 |
-
id: str = Field(pattern=r"^[a-z][a-z0-9-]{1,30}$")
|
| 270 |
-
group: str = Field(min_length=1, max_length=40)
|
| 271 |
-
about: str = Field("", max_length=80)
|
| 272 |
-
icon: str
|
| 273 |
-
color: str
|
| 274 |
-
ids: list[str] = Field(default_factory=list, max_length=80)
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
class Collections(BaseModel):
|
| 278 |
-
collections: list[Collection] = Field(max_length=20)
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
@router.get("/collections")
|
| 282 |
-
def get_collections(request: Request):
|
| 283 |
-
_admin(request)
|
| 284 |
-
return {"collections": catalog.collections(), "icons": catalog.ICONS, "colors": catalog.COLORS}
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
@router.put("/collections")
|
| 288 |
-
def put_collections(body: Collections, request: Request):
|
| 289 |
-
u = _admin(request)
|
| 290 |
-
ids = [c.id for c in body.collections]
|
| 291 |
-
if len(set(ids)) != len(ids):
|
| 292 |
-
raise HTTPException(400, "two collections share an id")
|
| 293 |
-
for c in body.collections:
|
| 294 |
-
if c.icon not in catalog.ICONS or c.color not in catalog.COLORS:
|
| 295 |
-
raise HTTPException(400, f"{c.group}: pick an icon and a colour from the lists")
|
| 296 |
-
bad = [k for k in c.ids if not KEY.match(k)]
|
| 297 |
-
if bad:
|
| 298 |
-
raise HTTPException(400, f"{c.group}: not environment ids: {', '.join(bad[:3])}")
|
| 299 |
-
settings.save(u["name"], {"collections": [c.model_dump() for c in body.collections]}, note="edited collections")
|
| 300 |
-
return {"collections": catalog.collections()}
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
# ── settings ─────────────────────────────────────────────────────────────────
|
| 304 |
-
def _settings() -> dict[str, Any]:
|
| 305 |
-
return {"rollouts_enabled": settings.get("rollouts_enabled", True),
|
| 306 |
-
"max_active": settings.get("max_active", config.MAX_ACTIVE_ROLLOUTS),
|
| 307 |
-
"max_per_user": settings.get("max_per_user", config.MAX_ACTIVE_PER_USER),
|
| 308 |
-
"agents": settings.get("agents", [a["id"] for a in runner.AGENTS]),
|
| 309 |
-
"announcement": settings.get("announcement", ""),
|
| 310 |
-
"all_agents": runner.AGENTS, "admin_org": settings.ADMIN_ORG}
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
class SettingsIn(BaseModel):
|
| 314 |
-
rollouts_enabled: bool
|
| 315 |
-
max_active: int = Field(ge=1, le=200)
|
| 316 |
-
max_per_user: int = Field(ge=1, le=20)
|
| 317 |
-
agents: list[str] = Field(max_length=20)
|
| 318 |
-
announcement: str = Field("", max_length=280)
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
@router.get("/settings")
|
| 322 |
-
def get_settings(request: Request):
|
| 323 |
-
_admin(request)
|
| 324 |
-
return _settings()
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
@router.put("/settings")
|
| 328 |
-
def put_settings(body: SettingsIn, request: Request):
|
| 329 |
-
u = _admin(request)
|
| 330 |
-
if any(a not in runner.AGENT_IDS for a in body.agents):
|
| 331 |
-
raise HTTPException(400, "unknown agent")
|
| 332 |
-
settings.save(u["name"], body.model_dump(), note="changed settings")
|
| 333 |
-
return _settings()
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
# ── audit and storage ────────────────────────────────────────────────────────
|
| 337 |
-
@router.get("/audit")
|
| 338 |
-
def audit(request: Request, limit: int = 200):
|
| 339 |
-
_admin(request)
|
| 340 |
-
return {"entries": settings.audit(max(1, min(limit, 1000)))}
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
@router.get("/indexes")
|
| 344 |
-
def indexes(request: Request):
|
| 345 |
-
_admin(request)
|
| 346 |
-
out = []
|
| 347 |
-
for f in sorted(config.INDEX_DIR.glob("*.json.gz")):
|
| 348 |
-
head = {}
|
| 349 |
-
try:
|
| 350 |
-
head = json.loads(f.with_name(f.name.replace(".json.gz", ".head.json")).read_text())
|
| 351 |
-
except (OSError, ValueError):
|
| 352 |
-
try:
|
| 353 |
-
idx = json.loads(gzip.decompress(f.read_bytes()))
|
| 354 |
-
head = {"tasks": len(idx.get("tasks") or []), "version": idx.get("version"), "built": idx.get("built")}
|
| 355 |
-
except (OSError, ValueError):
|
| 356 |
-
pass
|
| 357 |
-
pack = config.PACK_DIR / f.name
|
| 358 |
-
out.append({"spec": f.name[:-8].replace("__", "/", 1), "bytes": f.stat().st_size, "pack_bytes": pack.stat().st_size if pack.exists() else None,
|
| 359 |
-
"tasks": head.get("tasks"), "version": head.get("version"), "current": head.get("version") == catalog.INDEX_VERSION,
|
| 360 |
-
"built": head.get("built")})
|
| 361 |
-
return {"indexes": out, "version": catalog.INDEX_VERSION, "jobs": _jobs(), "operations": _operations()}
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
def _operations(envs=None):
|
| 365 |
-
"""Evidence from the shared store, not the admin process's worker settings."""
|
| 366 |
-
envs = envs if envs is not None else search_api.environments(include_hidden=True)
|
| 367 |
-
spaces = [e for e in envs if e["kind"] == "space"]
|
| 368 |
-
records = space_checks.inventory()
|
| 369 |
-
states = Counter(space_checks.status(records.get(e["id"])) for e in spaces)
|
| 370 |
-
versions = Counter(e.get("declared_version") or "Unknown" for e in spaces if space_checks.verified(records.get(e["id"])))
|
| 371 |
-
job = auto_index.status()
|
| 372 |
-
now = time.time()
|
| 373 |
-
stale = bool(job and now > (job.get("next_at") or job.get("started_at") or 0) + auto_index.INTERVAL)
|
| 374 |
-
return {"snapshot": snapshot.status(), "indexer": {**job, "stale": stale},
|
| 375 |
-
"checks": {"candidates": len(spaces), "ready": sum(space_checks.browseable(records.get(e["id"])) for e in spaces),
|
| 376 |
-
"openenv": sum(space_checks.verified(records.get(e["id"])) for e in spaces),
|
| 377 |
-
"statuses": dict(states), "versions": versions.most_common(), "fresh_seconds": space_checks.FRESH,
|
| 378 |
-
"latest_at": max((records.get(e["id"], {}).get("checked_at", 0) for e in spaces), default=0)},
|
| 379 |
-
"task_catalog": space_tasks.census(records, [e["id"] for e in spaces if e["key"] not in catalog.hidden() and space_checks.browseable(records.get(e["id"]))])}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/admin_app.py
DELETED
|
@@ -1,79 +0,0 @@
|
|
| 1 |
-
"""The HF RL Explorer admin: its own app, deployed as its own private Space, for members of the admin organization.
|
| 2 |
-
|
| 3 |
-
uv run uvicorn app.admin_app:app --port 8061 # locally, next to the explorer on 8060
|
| 4 |
-
|
| 5 |
-
It mounts the same bucket as the explorer (/data on the Space) and shares nothing else with it: no process, no
|
| 6 |
-
cookies (another origin), no rollout threads. What it changes goes into the bucket's settings file, which the
|
| 7 |
-
explorer re-reads (see app/admin.py). The Space being private already limits it to the organization; every API
|
| 8 |
-
route checks membership again.
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
from __future__ import annotations
|
| 12 |
-
|
| 13 |
-
import os
|
| 14 |
-
|
| 15 |
-
from fastapi import FastAPI, Request
|
| 16 |
-
from fastapi.middleware.gzip import GZipMiddleware
|
| 17 |
-
from fastapi.responses import FileResponse
|
| 18 |
-
from fastapi.staticfiles import StaticFiles
|
| 19 |
-
|
| 20 |
-
from . import admin, auth, config, http, settings, version
|
| 21 |
-
|
| 22 |
-
# where the public explorer is, for links to its datasets, Spaces and rollouts
|
| 23 |
-
EXPLORER = os.environ.get("RLX_EXPLORER_URL", "http://localhost:8060" if config.LOCAL_MODE else "https://fineenvs-rl-explorer.hf.space").rstrip("/")
|
| 24 |
-
|
| 25 |
-
app = FastAPI(title="HF RL Explorer admin", docs_url=None, redoc_url=None, openapi_url=None)
|
| 26 |
-
app.include_router(auth.router)
|
| 27 |
-
app.include_router(admin.router)
|
| 28 |
-
app.add_middleware(GZipMiddleware, minimum_size=2048, compresslevel=5)
|
| 29 |
-
http.install(app)
|
| 30 |
-
ADMIN_WEB = config.ROOT / "web-admin"
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
@app.on_event("startup")
|
| 34 |
-
def _startup() -> None:
|
| 35 |
-
import threading
|
| 36 |
-
|
| 37 |
-
from . import search_api, snapshot
|
| 38 |
-
|
| 39 |
-
config.STORAGE_DIR.mkdir(parents=True, exist_ok=True)
|
| 40 |
-
config.INDEX_DIR.mkdir(parents=True, exist_ok=True)
|
| 41 |
-
# the Hub listing the dashboard opens with: fetched now, so the first visit doesn't wait on it
|
| 42 |
-
snapshot.start()
|
| 43 |
-
threading.Thread(target=lambda: search_api.environments(include_hidden=True), daemon=True, name="warm").start()
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
@app.get("/healthz", include_in_schema=False)
|
| 47 |
-
def healthz():
|
| 48 |
-
"""Alive: the process answers."""
|
| 49 |
-
return {"ok": True}
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
@app.get("/readyz", include_in_schema=False)
|
| 53 |
-
def readyz():
|
| 54 |
-
"""Ready: the bucket it keeps the settings in is mounted and writable (this app runs no rollouts or indexes)."""
|
| 55 |
-
from fastapi.responses import JSONResponse
|
| 56 |
-
|
| 57 |
-
try:
|
| 58 |
-
settings.DIR.mkdir(parents=True, exist_ok=True)
|
| 59 |
-
(settings.DIR / ".ready").write_text("ok")
|
| 60 |
-
store = "ok"
|
| 61 |
-
except OSError as e:
|
| 62 |
-
store = f"unwritable: {type(e).__name__}"
|
| 63 |
-
return JSONResponse({"ok": store == "ok", "store": store}, status_code=200 if store == "ok" else 503)
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
@app.get("/api/me")
|
| 67 |
-
def me(request: Request):
|
| 68 |
-
u = auth.current_user(request)
|
| 69 |
-
return {"user": auth.public(u), "local": config.LOCAL_MODE, "oauth": not config.LOCAL_MODE, "is_admin": settings.is_admin(u),
|
| 70 |
-
"admin_org": settings.ADMIN_ORG, "explorer": EXPLORER, "version": version.app_version(), "source": version.source_hash()}
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
@app.get("/")
|
| 74 |
-
def index():
|
| 75 |
-
return FileResponse(ADMIN_WEB / "index.html")
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
app.mount("/admin", StaticFiles(directory=ADMIN_WEB), name="admin-web")
|
| 79 |
-
app.mount("/", StaticFiles(directory=config.WEB_DIR), name="web") # the explorer's stylesheets, icons and helpers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/settings.py
CHANGED
|
@@ -4,8 +4,8 @@ One JSON file in the bucket (STORAGE_DIR/admin/settings.json) holds the collecti
|
|
| 4 |
environments, the rollout switches and an announcement; every change is appended to admin/audit.jsonl with who
|
| 5 |
made it. Values not set here fall back to the defaults in code.
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
"""
|
| 10 |
|
| 11 |
from __future__ import annotations
|
|
@@ -19,8 +19,6 @@ from typing import Any
|
|
| 19 |
|
| 20 |
from . import config
|
| 21 |
|
| 22 |
-
ADMIN_ORG = os.environ.get("RLX_ADMIN_ORG", "FineEnvs")
|
| 23 |
-
EXTRA_ADMINS = {u.strip() for u in os.environ.get("RLX_ADMINS", "").split(",") if u.strip()}
|
| 24 |
DIR = config.STORAGE_DIR / "admin"
|
| 25 |
PATH = DIR / "settings.json"
|
| 26 |
AUDIT = DIR / "audit.jsonl"
|
|
@@ -90,37 +88,5 @@ def audit(limit: int = 200) -> list[dict[str, Any]]:
|
|
| 90 |
return out
|
| 91 |
|
| 92 |
|
| 93 |
-
# ── who is an admin ──────────────────────────────────────────────────────────
|
| 94 |
-
_members: tuple[float, set[str]] | None = None
|
| 95 |
-
_members_lock = threading.Lock()
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def _org_members() -> set[str]:
|
| 99 |
-
"""The admin organization's members, from the Hub, refreshed every 10 minutes."""
|
| 100 |
-
global _members
|
| 101 |
-
with _members_lock:
|
| 102 |
-
if _members and time.time() - _members[0] < 600:
|
| 103 |
-
return _members[1]
|
| 104 |
-
from huggingface_hub import HfApi
|
| 105 |
-
|
| 106 |
-
try:
|
| 107 |
-
names = {m.username for m in HfApi(token=False).list_organization_members(ADMIN_ORG)}
|
| 108 |
-
except Exception: # noqa: BLE001 - the Hub is down: keep the last list, else nobody
|
| 109 |
-
with _members_lock:
|
| 110 |
-
return _members[1] if _members else set()
|
| 111 |
-
with _members_lock:
|
| 112 |
-
_members = (time.time(), names)
|
| 113 |
-
return names
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
def is_admin(user: dict[str, Any] | None) -> bool:
|
| 117 |
-
"""Whether this signed-in user may open the admin dashboard. The name comes from the encrypted session, which
|
| 118 |
-
the Hub set at sign-in, so it can't be claimed by a request."""
|
| 119 |
-
if not user or not user.get("name"):
|
| 120 |
-
return False
|
| 121 |
-
# the org's public member list, or the memberships the Hub reported when this user signed in (private ones too)
|
| 122 |
-
return user["name"] in EXTRA_ADMINS or user["name"] in _org_members() or ADMIN_ORG in (user.get("orgs") or [])
|
| 123 |
-
|
| 124 |
-
|
| 125 |
def path_for(name: str) -> Path:
|
| 126 |
return DIR / name
|
|
|
|
| 4 |
environments, the rollout switches and an announcement; every change is appended to admin/audit.jsonl with who
|
| 5 |
made it. Values not set here fall back to the defaults in code.
|
| 6 |
|
| 7 |
+
The separate private admin application writes these settings; this public module
|
| 8 |
+
only supplies the shared storage contract.
|
| 9 |
"""
|
| 10 |
|
| 11 |
from __future__ import annotations
|
|
|
|
| 19 |
|
| 20 |
from . import config
|
| 21 |
|
|
|
|
|
|
|
| 22 |
DIR = config.STORAGE_DIR / "admin"
|
| 23 |
PATH = DIR / "settings.json"
|
| 24 |
AUDIT = DIR / "audit.jsonl"
|
|
|
|
| 88 |
return out
|
| 89 |
|
| 90 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
def path_for(name: str) -> Path:
|
| 92 |
return DIR / name
|
app/version.py
CHANGED
|
@@ -26,7 +26,7 @@ def app_version() -> str:
|
|
| 26 |
@lru_cache(maxsize=1)
|
| 27 |
def source_hash() -> str:
|
| 28 |
h = hashlib.sha256()
|
| 29 |
-
for base in (ROOT / "app", ROOT / "web"
|
| 30 |
for p in sorted(base.rglob("*")):
|
| 31 |
if p.is_file() and p.suffix in (".py", ".js", ".txt", ".json", ".css", ".html") and "__pycache__" not in p.parts:
|
| 32 |
h.update(p.relative_to(ROOT).as_posix().encode())
|
|
|
|
| 26 |
@lru_cache(maxsize=1)
|
| 27 |
def source_hash() -> str:
|
| 28 |
h = hashlib.sha256()
|
| 29 |
+
for base in (ROOT / "app", ROOT / "web"):
|
| 30 |
for p in sorted(base.rglob("*")):
|
| 31 |
if p.is_file() and p.suffix in (".py", ".js", ".txt", ".json", ".css", ".html") and "__pycache__" not in p.parts:
|
| 32 |
h.update(p.relative_to(ROOT).as_posix().encode())
|
scripts/check_judge_relay.py
DELETED
|
@@ -1,57 +0,0 @@
|
|
| 1 |
-
"""The judge relay end to end, minus the sandbox's network hop: a judge session made exactly as
|
| 2 |
-
app/runner.Rollout._judge_session makes it, then the call a task's grader makes (its variables bound by app/judge.py: the
|
| 3 |
-
per-rollout key, the relay URL, the judge model) to the capture proxy. Then a forged key, and the key after the session ends,
|
| 4 |
-
both refused. Uses your HF token (`hf auth login`) for one judge call; prints no token.
|
| 5 |
-
|
| 6 |
-
uv run python scripts/check_judge_relay.py path/to/task.toml
|
| 7 |
-
|
| 8 |
-
Last run 2026-10-03 on a MiMo General task: 200 with a correct grade in 49 s (Inkling is slow; the local gradio.live
|
| 9 |
-
tunnel cuts requests at 60 s, so long judge prompts need the Space's own /capture), forged 401, 1 call, deleted 401."""
|
| 10 |
-
import asyncio, os, secrets, sys, time
|
| 11 |
-
from dataclasses import replace
|
| 12 |
-
|
| 13 |
-
sys.path.insert(0, os.getcwd())
|
| 14 |
-
os.environ.setdefault("RLX_WARM", "0")
|
| 15 |
-
import httpx
|
| 16 |
-
from huggingface_hub import get_token
|
| 17 |
-
|
| 18 |
-
from app import config, judge, runner
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
async def main():
|
| 22 |
-
runner._proxy_url() # starts the capture server (and, locally, its tunnel)
|
| 23 |
-
svc = runner.service()
|
| 24 |
-
port = svc.capture.port
|
| 25 |
-
pool = svc.capture.app.state.upstreams
|
| 26 |
-
from openenv.core.harness.capture.sessions import Upstream
|
| 27 |
-
|
| 28 |
-
jup = Upstream(llm_url=config.ROUTER, model="thinkingmachines/Inkling", api_key=get_token(), provider="hf")
|
| 29 |
-
jclient, jlevel = await pool.resolve(jup)
|
| 30 |
-
sid = "j" + secrets.token_hex(16)
|
| 31 |
-
svc.capture.registry.create(sid, upstream=replace(jup, model=jclient.served_model or jup.model), capture_level=jlevel,
|
| 32 |
-
purpose="eval", max_model_calls=runner.JUDGE_MAX_CALLS, role="judge", run="local-test")
|
| 33 |
-
toml = open(sys.argv[1]).read()
|
| 34 |
-
b = judge.bindings(judge.plan(toml), relay=f"http://127.0.0.1:{port}", capability=sid, judge="thinkingmachines/Inkling")["verifier"]
|
| 35 |
-
url_key = next(k for k in b if k.endswith("_URL")); key_key = next(k for k in b if k.endswith("_KEY") and b[k] == sid)
|
| 36 |
-
model = next(v for k, v in b.items() if "MODEL" in k)
|
| 37 |
-
print("grader gets", {k: ("<capability>" if v == sid else v) for k, v in b.items()})
|
| 38 |
-
t = time.time()
|
| 39 |
-
async with httpx.AsyncClient(timeout=180) as c:
|
| 40 |
-
r = await c.post(b[url_key] + "/chat/completions", headers={"Authorization": f"Bearer {b[key_key]}"},
|
| 41 |
-
json={"model": model, "temperature": 0, "max_tokens": 200,
|
| 42 |
-
"messages": [{"role": "system", "content": "You grade answers. Reply with JSON {\"score\": 0 or 1, \"why\": str}."},
|
| 43 |
-
{"role": "user", "content": "Rubric: the answer states that 7*8=56. Answer: 7 times 8 is 56."}]})
|
| 44 |
-
# a forged key and a model the session doesn't serve
|
| 45 |
-
bad = await c.post(b[url_key] + "/chat/completions", headers={"Authorization": "Bearer jforged"}, json={"model": model, "messages": [{"role": "user", "content": "hi"}]})
|
| 46 |
-
print("judge reply", r.status_code, round(time.time() - t, 1), "s:", (r.json().get("choices") or [{}])[0].get("message", {}).get("content", r.text)[:200])
|
| 47 |
-
print("forged key ->", bad.status_code)
|
| 48 |
-
sess = svc.capture.registry.get(sid)
|
| 49 |
-
print("session calls:", sess.graph.stats()["n_turns"])
|
| 50 |
-
svc.capture.registry.delete(sid)
|
| 51 |
-
print("after delete, same key ->", httpx.post(b[url_key] + "/chat/completions", headers={"Authorization": f"Bearer {sid}"},
|
| 52 |
-
json={"model": model, "messages": [{"role": "user", "content": "hi"}]}, timeout=30).status_code)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
asyncio.run(main())
|
| 56 |
-
for t in __import__("gradio.tunneling", fromlist=["x"]).CURRENT_TUNNELS:
|
| 57 |
-
t.kill()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/deploy_spaces.py
DELETED
|
@@ -1,115 +0,0 @@
|
|
| 1 |
-
"""Deploy HF RL Explorer from this folder: the public explorer Space and its private admin Space.
|
| 2 |
-
|
| 3 |
-
uv run python scripts/deploy_spaces.py # the plan, nothing changed
|
| 4 |
-
uv run python scripts/deploy_spaces.py --apply # create or update both Spaces
|
| 5 |
-
uv run python scripts/deploy_spaces.py --apply --only admin # one of them
|
| 6 |
-
uv run python scripts/deploy_spaces.py --apply --listing-from-store # once the indexer Job runs (DESIGN.md)
|
| 7 |
-
|
| 8 |
-
Both run this image (Dockerfile); the admin with RLX_APP=app.admin_app:app. They share only the bucket
|
| 9 |
-
FineEnvs/rl-explorer-data, mounted read-write at /data; the explorer also mounts the MiMo explorer's repository
|
| 10 |
-
snapshots read-only (one copy, the one MiMo's own runs use). Each Space has its own SESSION_SECRET: created once,
|
| 11 |
-
never printed, never replaced (replacing it signs everyone out). Each Space gets one commit with its own README,
|
| 12 |
-
so one build, and files no longer here are removed from it.
|
| 13 |
-
"""
|
| 14 |
-
|
| 15 |
-
from __future__ import annotations
|
| 16 |
-
|
| 17 |
-
import argparse
|
| 18 |
-
import fnmatch
|
| 19 |
-
import os
|
| 20 |
-
import secrets
|
| 21 |
-
import sys
|
| 22 |
-
from pathlib import Path
|
| 23 |
-
|
| 24 |
-
from huggingface_hub import CommitOperationAdd, CommitOperationDelete, HfApi, Volume
|
| 25 |
-
|
| 26 |
-
ROOT = Path(__file__).resolve().parents[1]
|
| 27 |
-
BUCKET = "FineEnvs/rl-explorer-data"
|
| 28 |
-
EXPLORER_URL = "https://fineenvs-rl-explorer.hf.space"
|
| 29 |
-
DATA = Volume(type="bucket", source=BUCKET, mount_path="/data")
|
| 30 |
-
SPACES = {
|
| 31 |
-
"explorer": {
|
| 32 |
-
"repo": "FineEnvs/RL-Explorer", "private": False, "readme": "SPACE_README.md", "hardware": "cpu-basic",
|
| 33 |
-
"variables": {"MIMO_SNAPSHOT_DIR": "/mimo-snapshots"},
|
| 34 |
-
"volumes": [DATA, Volume(type="bucket", source="FineEnvs/mimo-explorer-runs", mount_path="/mimo-snapshots",
|
| 35 |
-
path="repo-snapshots", read_only=True)],
|
| 36 |
-
},
|
| 37 |
-
"admin": {
|
| 38 |
-
"repo": "FineEnvs/RL-Explorer-admin", "private": True, "readme": "ADMIN_SPACE_README.md", "hardware": "cpu-basic",
|
| 39 |
-
"variables": {"RLX_APP": "app.admin_app:app", "RLX_OAUTH_SCOPES": "openid profile read-memberships", "RLX_EXPLORER_URL": EXPLORER_URL},
|
| 40 |
-
"volumes": [DATA],
|
| 41 |
-
},
|
| 42 |
-
}
|
| 43 |
-
# never uploaded: local data and caches, the READMEs (each Space gets its own as README.md), editor and OS files
|
| 44 |
-
IGNORE = [".venv/*", ".local-data/*", "*/__pycache__/*", "__pycache__/*", "*.pyc", ".pytest_cache/*", ".ruff_cache/*",
|
| 45 |
-
".gradio/*", "node_modules/*", ".DS_Store", "*/.DS_Store", "README.md", "SPACE_README.md", "ADMIN_SPACE_README.md", "EXPLORER_AUDIT.md",
|
| 46 |
-
".env", ".env.*", "*/.env", "*/.env.*", "*.env", "*.pem", "*.key", ".git/*"]
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
SKIP_DIRS = {".venv", ".local-data", "__pycache__", ".pytest_cache", ".ruff_cache", ".gradio", "node_modules", ".git"}
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def files() -> list[str]:
|
| 53 |
-
"""What goes into the Space: walked without descending into local data, caches or the virtualenv."""
|
| 54 |
-
out = []
|
| 55 |
-
for root, dirs, names in os.walk(ROOT):
|
| 56 |
-
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS and not (Path(root) / d).is_symlink())
|
| 57 |
-
for n in sorted(names):
|
| 58 |
-
rel = (Path(root) / n).relative_to(ROOT).as_posix()
|
| 59 |
-
if not (Path(root) / n).is_symlink() and not any(fnmatch.fnmatch(rel, pat) for pat in IGNORE):
|
| 60 |
-
out.append(rel)
|
| 61 |
-
return out
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def deploy(api: HfApi, name: str, spec: dict, listing_from_store: bool, apply: bool) -> None:
|
| 65 |
-
repo = spec["repo"]
|
| 66 |
-
variables = {**spec["variables"], **({"RLX_LISTING_FROM_STORE": "1"} if listing_from_store else {})}
|
| 67 |
-
paths = files()
|
| 68 |
-
print(f"\n{name}: {repo} ({'private' if spec['private'] else 'public'}, {spec['hardware']})")
|
| 69 |
-
print(f" volumes: " + "; ".join(f"{v.source}{'/' + v.path if v.path else ''} -> {v.mount_path}{' (read-only)' if v.read_only else ''}" for v in spec["volumes"]))
|
| 70 |
-
print(f" variables: {variables}")
|
| 71 |
-
print(f" files: {len(paths)} + README.md from {spec['readme']}")
|
| 72 |
-
if not apply:
|
| 73 |
-
return
|
| 74 |
-
api.create_repo(repo, repo_type="space", space_sdk="docker", private=spec["private"], exist_ok=True,
|
| 75 |
-
space_hardware=spec["hardware"])
|
| 76 |
-
info = api.space_info(repo)
|
| 77 |
-
if bool(info.private) != spec["private"]:
|
| 78 |
-
api.update_repo_settings(repo, repo_type="space", private=spec["private"])
|
| 79 |
-
api.set_space_volumes(repo, spec["volumes"])
|
| 80 |
-
for k, v in variables.items():
|
| 81 |
-
api.add_space_variable(repo, k, v)
|
| 82 |
-
if "SESSION_SECRET" not in {getattr(x, "key", x) for x in api.get_space_secrets(repo)}: # names, or objects with .key
|
| 83 |
-
api.add_space_secret(repo, "SESSION_SECRET", secrets.token_urlsafe(48), description="signs this Space's sessions")
|
| 84 |
-
print(" SESSION_SECRET: created")
|
| 85 |
-
runtime = api.get_space_runtime(repo)
|
| 86 |
-
if (runtime.requested_hardware or runtime.hardware) != spec["hardware"]:
|
| 87 |
-
api.request_space_hardware(repo, spec["hardware"])
|
| 88 |
-
ops = [CommitOperationAdd(path_in_repo=rel, path_or_fileobj=str(ROOT / rel)) for rel in paths]
|
| 89 |
-
ops.append(CommitOperationAdd(path_in_repo="README.md", path_or_fileobj=str(ROOT / spec["readme"])))
|
| 90 |
-
keep = set(paths) | {"README.md", ".gitattributes"}
|
| 91 |
-
ops += [CommitOperationDelete(path_in_repo=f) for f in api.list_repo_files(repo, repo_type="space") if f not in keep]
|
| 92 |
-
commit = api.create_commit(repo, repo_type="space", operations=ops, commit_message="Deploy HF RL Explorer")
|
| 93 |
-
print(f" pushed {commit.oid[:10]}: https://huggingface.co/spaces/{repo}")
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def main() -> None:
|
| 97 |
-
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 98 |
-
ap.add_argument("--apply", action="store_true", help="create or update the Spaces (without it, print the plan)")
|
| 99 |
-
ap.add_argument("--only", choices=sorted(SPACES), help="deploy one Space")
|
| 100 |
-
ap.add_argument("--listing-from-store", action="store_true", help="read the indexer Job's listing (set once the Job runs)")
|
| 101 |
-
args = ap.parse_args()
|
| 102 |
-
api = HfApi()
|
| 103 |
-
me = api.whoami()
|
| 104 |
-
orgs = {o["name"]: o.get("roleInOrg") for o in me.get("orgs", [])}
|
| 105 |
-
if orgs.get("FineEnvs") not in ("admin", "write", "contributor"):
|
| 106 |
-
sys.exit(f"{me['name']} can't write to FineEnvs (role: {orgs.get('FineEnvs')})")
|
| 107 |
-
for name, spec in SPACES.items():
|
| 108 |
-
if not args.only or args.only == name:
|
| 109 |
-
deploy(api, name, spec, args.listing_from_store, args.apply)
|
| 110 |
-
if not args.apply:
|
| 111 |
-
print("\nNothing changed: run with --apply.")
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
if __name__ == "__main__":
|
| 115 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scripts/schedule_indexer.py
DELETED
|
@@ -1,146 +0,0 @@
|
|
| 1 |
-
"""Schedule the catalog indexer (app/indexer.py) as an HF scheduled Job. Prints what it would create; --apply creates it.
|
| 2 |
-
|
| 3 |
-
uv run python scripts/schedule_indexer.py # print the Job and the equivalent `hf` command
|
| 4 |
-
uv run python scripts/schedule_indexer.py --apply # create it (asks nothing; needs write access to
|
| 5 |
-
# FineEnvs' Jobs, and the explorer Space built)
|
| 6 |
-
uv run python scripts/schedule_indexer.py --apply --replace # replace one created before (same name label)
|
| 7 |
-
|
| 8 |
-
What the Job is:
|
| 9 |
-
|
| 10 |
-
image the explorer Space's own image, `hf.co/spaces/FineEnvs/RL-Explorer` (same code, same lockfile, already built
|
| 11 |
-
by the Space; a Job can run any Space's image). The Space must exist and have built once.
|
| 12 |
-
command `python -m app.indexer --store /data --budget-min 40 --use-token`, from the image's app folder through
|
| 13 |
-
uv, like the Space's own CMD.
|
| 14 |
-
schedule @hourly, with concurrency off: a run still going when the next is due makes the next one wait, never two
|
| 15 |
-
writers. The run's own budget (40 min of index rebuilds) and the Job's timeout (55 min) keep it inside the
|
| 16 |
-
hour.
|
| 17 |
-
flavor cpu-upgrade (8 vCPU, 32 GB): the full listing (every Space with its files) and a big dataset's index
|
| 18 |
-
(13,825 tasks) fit with room to spare; SQLite builds on the container's local disk.
|
| 19 |
-
volume the private bucket FineEnvs/rl-explorer-data mounted read-write at /data (`Volume(type="bucket")`, the
|
| 20 |
-
platform's hf-mount, the same volume the explorer and admin Spaces mount). STORAGE_DIR=/data, so catalog's
|
| 21 |
-
own writers put indexes, packs and listing.json.gz exactly where the Spaces read them.
|
| 22 |
-
secret HF_TOKEN, a token of the Job's owner: raises the Hub's rate limits for downloading public dataset files
|
| 23 |
-
(--use-token; listings and access checks stay anonymous, so nothing private is ever indexed). It's sent
|
| 24 |
-
as a Job secret (encrypted, not shown in the Job's settings) and never printed here.
|
| 25 |
-
|
| 26 |
-
How the Job writes to the bucket. Two ways exist: the mounted volume (files written under /data are uploaded when
|
| 27 |
-
closed; a rename is one bucket batch, adding the new path and deleting the old, so temp-then-rename is atomic for
|
| 28 |
-
readers; other mounts see a change within their metadata TTL, ~10 s, and a full listing poll, ~30 s), or the bucket API
|
| 29 |
-
(`HfApi.batch_bucket_files`, one commit per call). The indexer writes through the mount: catalog's index writers
|
| 30 |
-
already work that way on the Spaces, and its snapshot store (`--store /data`) writes the database to a temp name,
|
| 31 |
-
renames it into `snapshots/`, checks it landed whole, then writes the pointer `snapshots/LATEST.v<SCHEMA>.json` last
|
| 32 |
-
the same way. `--store hf://buckets/FineEnvs/rl-explorer-data` publishes the snapshot through the bucket API instead
|
| 33 |
-
(the same two-step order: database, then pointer) if the mount ever proves unreliable for a large file.
|
| 34 |
-
|
| 35 |
-
The apps pick a new snapshot up on their own: each looks at the pointer every 60 s (app/snapshot.py). Nothing else
|
| 36 |
-
changes on the Spaces.
|
| 37 |
-
"""
|
| 38 |
-
|
| 39 |
-
from __future__ import annotations
|
| 40 |
-
|
| 41 |
-
import argparse
|
| 42 |
-
import json
|
| 43 |
-
import shlex
|
| 44 |
-
import sys
|
| 45 |
-
|
| 46 |
-
NAMESPACE = "FineEnvs"
|
| 47 |
-
NAME = "rl-explorer-indexer"
|
| 48 |
-
IMAGE = "hf.co/spaces/FineEnvs/RL-Explorer"
|
| 49 |
-
BUCKET = "FineEnvs/rl-explorer-data"
|
| 50 |
-
APP_DIR = "/home/user/app" # the Dockerfile's WORKDIR
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def spec(args: argparse.Namespace) -> dict:
|
| 54 |
-
run = (f"cd {APP_DIR} && exec uv run --frozen --no-dev python -m app.indexer --store /data "
|
| 55 |
-
f"--budget-min {args.budget_min:g} --max-builds {args.max_builds} --keep {args.keep}"
|
| 56 |
-
+ (" --use-token" if args.with_token else ""))
|
| 57 |
-
return {
|
| 58 |
-
"namespace": args.namespace,
|
| 59 |
-
"name": args.name,
|
| 60 |
-
"image": args.image,
|
| 61 |
-
"command": ["sh", "-c", run],
|
| 62 |
-
"schedule": args.schedule,
|
| 63 |
-
"concurrency": False,
|
| 64 |
-
"flavor": args.flavor,
|
| 65 |
-
"timeout": args.timeout,
|
| 66 |
-
"env": {"STORAGE_DIR": "/data", "RLX_CACHE_DIR": "/tmp/rlx-cache", "HF_HOME": "/tmp/hf-home", "PYTHONUNBUFFERED": "1"},
|
| 67 |
-
# anonymous by default: public datasets need no token. --with-token sends RLX_INDEXER_TOKEN (a read-only token
|
| 68 |
-
# made for this) as HF_TOKEN, for the Hub's higher rate limits; never your login token, which can write
|
| 69 |
-
"secrets": ["HF_TOKEN"] if args.with_token else [],
|
| 70 |
-
"volumes": [{"type": "bucket", "source": args.bucket, "mount_path": "/data"}],
|
| 71 |
-
"labels": {"app": "rl-explorer", "role": "indexer"},
|
| 72 |
-
}
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def cli(s: dict) -> str:
|
| 76 |
-
"""The same Job with the `hf` CLI."""
|
| 77 |
-
parts = ["hf", "jobs", "scheduled", "run", s["schedule"], "--namespace", s["namespace"], "--name", s["name"],
|
| 78 |
-
"--flavor", s["flavor"], "--timeout", s["timeout"], "--no-concurrency",
|
| 79 |
-
"-v", f"hf://buckets/{s['volumes'][0]['source']}:{s['volumes'][0]['mount_path']}"]
|
| 80 |
-
for k, v in s["env"].items():
|
| 81 |
-
parts += ["-e", f"{k}={v}"]
|
| 82 |
-
for k in s["secrets"]:
|
| 83 |
-
parts += ["--secrets", k]
|
| 84 |
-
for k, v in s["labels"].items():
|
| 85 |
-
parts += ["-l", f"{k}={v}"]
|
| 86 |
-
parts += [s["image"], *s["command"]]
|
| 87 |
-
return " ".join(shlex.quote(p) for p in parts)
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def apply(s: dict, replace: bool) -> int:
|
| 91 |
-
from huggingface_hub import HfApi, Volume, get_token
|
| 92 |
-
|
| 93 |
-
import os
|
| 94 |
-
|
| 95 |
-
token = get_token()
|
| 96 |
-
if not token:
|
| 97 |
-
print("No HF token: run `hf auth login` (a token that can create Jobs in the FineEnvs organization).", file=sys.stderr)
|
| 98 |
-
return 2
|
| 99 |
-
job_token = os.environ.get("RLX_INDEXER_TOKEN")
|
| 100 |
-
if s["secrets"] and not job_token:
|
| 101 |
-
print("--with-token needs RLX_INDEXER_TOKEN: a read-only token made for the indexer (not your login token).", file=sys.stderr)
|
| 102 |
-
return 2
|
| 103 |
-
api = HfApi(token=token)
|
| 104 |
-
existing = [j for j in api.list_scheduled_jobs(namespace=s["namespace"], labels={"name": s["name"]})]
|
| 105 |
-
if existing and not replace:
|
| 106 |
-
print(f"A scheduled Job named {s['name']} exists already ({', '.join(j.id for j in existing)}): --replace to replace it.",
|
| 107 |
-
file=sys.stderr)
|
| 108 |
-
return 1
|
| 109 |
-
for j in existing:
|
| 110 |
-
api.delete_scheduled_job(scheduled_job_id=j.id, namespace=s["namespace"])
|
| 111 |
-
print(f"deleted scheduled Job {j.id}")
|
| 112 |
-
job = api.create_scheduled_job(
|
| 113 |
-
image=s["image"], command=s["command"], schedule=s["schedule"], concurrency=s["concurrency"], env=s["env"],
|
| 114 |
-
secrets={k: job_token for k in s["secrets"]} or None, flavor=s["flavor"], timeout=s["timeout"], name=s["name"],
|
| 115 |
-
labels=s["labels"], volumes=[Volume(**v) for v in s["volumes"]], namespace=s["namespace"])
|
| 116 |
-
print(f"created scheduled Job {job.id} ({s['schedule']}); runs: hf jobs scheduled inspect {job.id} --namespace {s['namespace']}")
|
| 117 |
-
return 0
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def main(argv: list[str] | None = None) -> int:
|
| 121 |
-
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 122 |
-
ap.add_argument("--apply", action="store_true", help="create the scheduled Job (otherwise only print it)")
|
| 123 |
-
ap.add_argument("--replace", action="store_true", help="with --apply: delete a scheduled Job of the same name first")
|
| 124 |
-
ap.add_argument("--with-token", action="store_true", help="send RLX_INDEXER_TOKEN (a read-only token) as the Job's HF_TOKEN")
|
| 125 |
-
ap.add_argument("--namespace", default=NAMESPACE)
|
| 126 |
-
ap.add_argument("--name", default=NAME)
|
| 127 |
-
ap.add_argument("--image", default=IMAGE, help="a Docker image with this app: the explorer Space's, by default")
|
| 128 |
-
ap.add_argument("--bucket", default=BUCKET)
|
| 129 |
-
ap.add_argument("--schedule", default="@hourly", help='a preset (@hourly) or a cron expression ("17 * * * *")')
|
| 130 |
-
ap.add_argument("--flavor", default="cpu-upgrade")
|
| 131 |
-
ap.add_argument("--timeout", default="55m")
|
| 132 |
-
ap.add_argument("--budget-min", type=float, default=40, help="minutes of index rebuilds per run")
|
| 133 |
-
ap.add_argument("--max-builds", type=int, default=100)
|
| 134 |
-
ap.add_argument("--keep", type=int, default=5, help="snapshots kept in the bucket")
|
| 135 |
-
args = ap.parse_args(argv)
|
| 136 |
-
s = spec(args)
|
| 137 |
-
print(json.dumps(s, indent=1))
|
| 138 |
-
print("\n# the same with the hf CLI:\n" + cli(s))
|
| 139 |
-
if not args.apply:
|
| 140 |
-
print("\n# nothing created: run again with --apply to create it")
|
| 141 |
-
return 0
|
| 142 |
-
return apply(s, args.replace)
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
if __name__ == "__main__":
|
| 146 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/conftest.py
DELETED
|
@@ -1,9 +0,0 @@
|
|
| 1 |
-
"""One sandbox for every test module: storage and caches in a temp folder, a fixed session secret, no warm-up."""
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
import tempfile
|
| 5 |
-
|
| 6 |
-
_TMP = tempfile.mkdtemp(prefix="rlx-test-")
|
| 7 |
-
os.environ.setdefault("RLX_TEST_TMP", _TMP)
|
| 8 |
-
os.environ.update(STORAGE_DIR=os.environ["RLX_TEST_TMP"], RLX_CACHE_DIR=f"{os.environ['RLX_TEST_TMP']}/cache", OAUTH_CLIENT_ID="test-client",
|
| 9 |
-
SESSION_SECRET="test-secret", RLX_WARM="0", RLX_ADMIN_ORG="FineEnvs")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/reward-stats.test.mjs
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
import test from 'node:test';
|
| 2 |
-
import assert from 'node:assert/strict';
|
| 3 |
-
import { rewardDistribution } from '../web/js/reward-stats.js';
|
| 4 |
-
|
| 5 |
-
test('negative and unbounded rewards remain in the histogram', () => {
|
| 6 |
-
const d = rewardDistribution([-100, -5, 0, 0.5, 1, 500]);
|
| 7 |
-
assert.equal(d.min, -100);
|
| 8 |
-
assert.equal(d.max, 500);
|
| 9 |
-
assert.equal(d.hist.reduce((a, b) => a+b), 6);
|
| 10 |
-
assert.equal(d.hist[0], 1);
|
| 11 |
-
assert.equal(d.hist[9], 1);
|
| 12 |
-
});
|
| 13 |
-
test('empty and normalized rewards keep a useful finite range', () => {
|
| 14 |
-
for (const xs of [[], [0, 0.5, 1], [NaN, Infinity]]) {
|
| 15 |
-
const d = rewardDistribution(xs);
|
| 16 |
-
assert.equal(d.min, 0);
|
| 17 |
-
assert.equal(d.max, 1);
|
| 18 |
-
assert.ok(d.hist.every(Number.isFinite));
|
| 19 |
-
}
|
| 20 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/schemaform.test.mjs
DELETED
|
@@ -1,89 +0,0 @@
|
|
| 1 |
-
import test from 'node:test';
|
| 2 |
-
import assert from 'node:assert/strict';
|
| 3 |
-
import { schemaForm, readForm, resolveSchema, validateValue } from '../web/js/schemaform.js';
|
| 4 |
-
|
| 5 |
-
const form = (fields = [], root = null) => ({
|
| 6 |
-
querySelector: () => root,
|
| 7 |
-
querySelectorAll: () => fields.map(([k, t, value]) => ({ dataset: { k, t }, value })),
|
| 8 |
-
});
|
| 9 |
-
const plain = (v) => JSON.parse(JSON.stringify(v));
|
| 10 |
-
|
| 11 |
-
test('optional booleans can be omitted, and explicit false remains false', () => {
|
| 12 |
-
const schema = { type: 'object', properties: { enabled: { type: 'boolean', default: true } } };
|
| 13 |
-
assert.deepEqual(plain(readForm(form([['enabled', 'bool', '']]), schema)), {});
|
| 14 |
-
assert.deepEqual(plain(readForm(form([['enabled', 'bool', 'false']]), schema)), { enabled: false });
|
| 15 |
-
assert.match(schemaForm(schema), /Use server default/);
|
| 16 |
-
});
|
| 17 |
-
|
| 18 |
-
test('integer inputs reject fractions rather than truncating actions', () => {
|
| 19 |
-
const s = { type: 'object', properties: { action: { type: 'integer', minimum: 0, maximum: 3 } }, required: ['action'] };
|
| 20 |
-
for (const value of ['1.5', '4', '-1', 'Infinity', '9007199254740992', '']) {
|
| 21 |
-
assert.throws(() => readForm(form([['action', 'int', value]]), s));
|
| 22 |
-
}
|
| 23 |
-
assert.equal(readForm(form([['action', 'int', '0']]), s).action, 0);
|
| 24 |
-
});
|
| 25 |
-
|
| 26 |
-
test('enum values retain their JSON type and never silently pick the first option', () => {
|
| 27 |
-
const s = { type: 'object', properties: { action: { enum: [0, 1, 'stop', null] } }, required: ['action'] };
|
| 28 |
-
assert.match(schemaForm(s), /Choose a value/);
|
| 29 |
-
assert.equal(readForm(form([['action', 'enum', '0']]), s).action, 0);
|
| 30 |
-
assert.equal(readForm(form([['action', 'enum', 'null']]), s).action, null);
|
| 31 |
-
assert.throws(() => readForm(form([['action', 'enum', '"bad"']]), s));
|
| 32 |
-
});
|
| 33 |
-
|
| 34 |
-
test('local references retain defaults and referenced discriminators', () => {
|
| 35 |
-
const s = { type: 'object', $defs: { Mode: { const: 'move' }, Count: { type: 'integer', default: 7 } },
|
| 36 |
-
properties: { type: { $ref: '#/$defs/Mode' }, count: { $ref: '#/$defs/Count' } } };
|
| 37 |
-
assert.match(schemaForm(s), /value="7"/);
|
| 38 |
-
assert.equal(readForm(form(), s).type, 'move');
|
| 39 |
-
});
|
| 40 |
-
|
| 41 |
-
test('recursive and missing references are bounded', () => {
|
| 42 |
-
const s = { $ref: '#/$defs/Loop', $defs: { Loop: { $ref: '#/$defs/Loop' } } };
|
| 43 |
-
assert.doesNotThrow(() => schemaForm(s));
|
| 44 |
-
assert.deepEqual(resolveSchema({ $ref: '#/$defs/missing' }, s), {});
|
| 45 |
-
});
|
| 46 |
-
|
| 47 |
-
test('root action unions use JSON without losing nested arguments', () => {
|
| 48 |
-
const s = { oneOf: [
|
| 49 |
-
{ type: 'object', properties: { type: { const: 'move' }, position: { type: 'array', items: { type: 'number' }, minItems: 2 } }, required: ['type', 'position'] },
|
| 50 |
-
{ type: 'object', properties: { type: { const: 'stop' } }, required: ['type'] },
|
| 51 |
-
] };
|
| 52 |
-
assert.match(schemaForm(s), /data-root-json/);
|
| 53 |
-
assert.deepEqual(readForm(form([], { value: '{"type":"move","position":[1,2]}' }), s), { type: 'move', position: [1, 2] });
|
| 54 |
-
assert.throws(() => readForm(form([], { value: '{"type":"move"}' }), s));
|
| 55 |
-
});
|
| 56 |
-
|
| 57 |
-
test('open objects have a JSON editor rather than claiming no inputs', () => {
|
| 58 |
-
assert.match(schemaForm({ type: 'object' }), /data-root-json/);
|
| 59 |
-
assert.match(schemaForm({ type: 'object', additionalProperties: false }), /No inputs/);
|
| 60 |
-
});
|
| 61 |
-
|
| 62 |
-
test('all interpolated upstream schema values are escaped', () => {
|
| 63 |
-
const payload = '\"><img src=https://evil.test/>';
|
| 64 |
-
const s = { type: 'object', properties: { [payload]: { type: 'number', default: payload, minimum: payload } } };
|
| 65 |
-
const html = schemaForm(s, { prefix: payload });
|
| 66 |
-
assert.ok(!html.includes('<img'));
|
| 67 |
-
assert.ok(!html.includes('min="' + payload));
|
| 68 |
-
});
|
| 69 |
-
|
| 70 |
-
test('sanitized property names cannot create duplicate input ids', () => {
|
| 71 |
-
const html = schemaForm({ properties: { 'a.b': { type: 'string' }, 'a/b': { type: 'string' } } });
|
| 72 |
-
const ids = [...html.matchAll(/\bid="([^"]+)"/g)].map((x) => x[1]);
|
| 73 |
-
assert.equal(ids.length, new Set(ids).size);
|
| 74 |
-
});
|
| 75 |
-
|
| 76 |
-
test('prototype-like field names remain ordinary JSON properties', () => {
|
| 77 |
-
const schema = JSON.parse('{"type":"object","properties":{"__proto__":{"type":"object"}}}');
|
| 78 |
-
const result = readForm(form([['__proto__', 'json', '{"polluted":true}']]), schema);
|
| 79 |
-
assert.equal(Object.getPrototypeOf(result), null);
|
| 80 |
-
assert.equal({}.polluted, undefined);
|
| 81 |
-
assert.equal(JSON.parse(JSON.stringify(result)).__proto__.polluted, true);
|
| 82 |
-
});
|
| 83 |
-
|
| 84 |
-
test('nested required, size and additional-properties constraints report actionable errors', () => {
|
| 85 |
-
const s = { type: 'object', properties: { args: { type: 'object', properties: { text: { type: 'string', minLength: 2 } }, required: ['text'], additionalProperties: false } } };
|
| 86 |
-
assert.throws(() => validateValue({ args: {} }, s), /args.text is required/);
|
| 87 |
-
assert.throws(() => validateValue({ args: { text: '' } }, s), /at least 2/);
|
| 88 |
-
assert.throws(() => validateValue({ args: { text: 'ok', answer: 'secret' } }, s), /not an allowed field/);
|
| 89 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/space-media.test.mjs
DELETED
|
@@ -1,24 +0,0 @@
|
|
| 1 |
-
import test from 'node:test';
|
| 2 |
-
import assert from 'node:assert/strict';
|
| 3 |
-
import { spaceMedia } from '../web/js/space-media.js';
|
| 4 |
-
import { taskContent } from '../web/js/space-task-view.js';
|
| 5 |
-
|
| 6 |
-
test('lazy Space media resolves against the serving Space without mutating observations', () => {
|
| 7 |
-
const raw = { observation: { asset_path: '/assets/hash?task_id=a', mime: 'audio/wav' } };
|
| 8 |
-
const out = spaceMedia(raw, 'https://owner-env.hf.space');
|
| 9 |
-
assert.equal(out.observation.media_url, 'https://owner-env.hf.space/assets/hash?task_id=a');
|
| 10 |
-
assert.equal(raw.observation.media_url, undefined);
|
| 11 |
-
});
|
| 12 |
-
|
| 13 |
-
test('media paths cannot change origin or use active URL schemes', () => {
|
| 14 |
-
for (const path of ['//evil.example/assets/a', 'javascript:alert(1)', 'https://evil.example/a', '/other/a'])
|
| 15 |
-
assert.equal(spaceMedia({ asset_path: path, mime: 'audio/wav' }, 'https://owner-env.hf.space').media_url, undefined);
|
| 16 |
-
assert.equal(spaceMedia({ asset_path: '/assets/a', mime: 'audio/wav' }, 'http://localhost').media_url, undefined);
|
| 17 |
-
});
|
| 18 |
-
|
| 19 |
-
test('task inputs are escaped and lazy media remains an available episode', () => {
|
| 20 |
-
const html = taskContent({ prompt: '<img src=x onerror=alert(1)>', task_id: 'a'.repeat(200), media_ready: false });
|
| 21 |
-
assert.ok(!html.includes('<img src=x'));
|
| 22 |
-
assert.ok(html.includes('Media loads when the episode starts'));
|
| 23 |
-
assert.ok(html.includes('Task record'));
|
| 24 |
-
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_admin_dashboard.py
DELETED
|
@@ -1,94 +0,0 @@
|
|
| 1 |
-
import time
|
| 2 |
-
from contextlib import contextmanager
|
| 3 |
-
|
| 4 |
-
import pytest
|
| 5 |
-
from fastapi.testclient import TestClient
|
| 6 |
-
|
| 7 |
-
from app import admin, admin_app, auth, auto_index, catalog, community, search_api, settings, snapshot, space_checks, store
|
| 8 |
-
|
| 9 |
-
client = TestClient(admin_app.app)
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
@pytest.fixture
|
| 13 |
-
def data(monkeypatch):
|
| 14 |
-
now = time.time()
|
| 15 |
-
checks = {name: {'id': name, 'stage': 'RUNNING', 'status': status, 'checked_at': at, 'interface': interface,
|
| 16 |
-
'task_catalog': {'tasks': 50, 'counted_splits': 1, 'complete': True, 'checked_at': now}}
|
| 17 |
-
for name, status, at, interface in [('org/openenv', space_checks.PASS, now, None),
|
| 18 |
-
('org/stale', space_checks.PASS, now-7200, None), ('org/nemo', 'Checks failed', now, 'nemo-gym')]}
|
| 19 |
-
envs = [{'id':'org/harbor','key':'org/harbor','kind':'dataset','framework':'harbor','indexed':{'tasks':8}}]
|
| 20 |
-
envs += [{'id':name, 'key':'space:'+name, 'kind':'space', 'api_check':rec, 'declared_version':'0.3.0'} for name,rec in checks.items()]
|
| 21 |
-
base = {'user':'alice','dataset':'org/harbor','path':'task','title':'Example','status':'done','reward':1,
|
| 22 |
-
'visibility':'public','created_at':now,'updated_at':now,'avatar':'https://example.com/avatar.png'}
|
| 23 |
-
runs = [{**base,'id':'harbor','runner':'harbor','reward':-2}, {**base,'id':'mimo','runner':'mimo','domain':'code','visibility':'private'},
|
| 24 |
-
{**base,'id':'nemo','runner':'nemo_gym'}, {**base,'id':'hidden','runner':'harbor'},
|
| 25 |
-
{**base,'id':'restricted','runner':'harbor','restricted':True}, {**base,'id':'ungraded','reward':None},
|
| 26 |
-
{**base,'id':'live','runner':'mimo','status':'running','reward':None}]
|
| 27 |
-
monkeypatch.setattr(auth,'current_user',lambda request:{'name':'alice'})
|
| 28 |
-
monkeypatch.setattr(settings,'is_admin',lambda u:True)
|
| 29 |
-
monkeypatch.setattr(settings,'get',lambda k,d=None: ['hidden'] if k=='hidden_runs' else d)
|
| 30 |
-
monkeypatch.setattr(admin,'_runs',lambda:runs)
|
| 31 |
-
monkeypatch.setattr(admin,'avatar',lambda u:None)
|
| 32 |
-
monkeypatch.setattr(search_api,'environments',lambda **kw:envs)
|
| 33 |
-
monkeypatch.setattr(space_checks,'inventory',lambda:checks)
|
| 34 |
-
monkeypatch.setattr(catalog,'hidden',lambda:set())
|
| 35 |
-
monkeypatch.setattr(auto_index,'status',lambda:{'state':'idle','last_success':now-20,'next_at':now+600})
|
| 36 |
-
monkeypatch.setattr(snapshot,'status',lambda:{'built_at':'2026-10-04T00:00:00+00:00'})
|
| 37 |
-
@contextmanager
|
| 38 |
-
def use():
|
| 39 |
-
yield None,None
|
| 40 |
-
monkeypatch.setattr(snapshot,'use',use)
|
| 41 |
-
monkeypatch.setattr(search_api,'sql_extra',lambda conn,ctx,n,q:{'stats':{'datasets':1,'spaces':2,'tasks':8}})
|
| 42 |
-
return envs,runs,checks
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def test_overview_matches_community_eligibility_and_checked_spaces(data):
|
| 46 |
-
d=client.get('/api/admin/overview').json()
|
| 47 |
-
assert d['environments']['spaces']==3
|
| 48 |
-
assert d['environments']['checked_spaces']==2
|
| 49 |
-
assert d['environments']['openenv']==1
|
| 50 |
-
assert d['environments']['public']=={'datasets':1,'spaces':2,'tasks':8}
|
| 51 |
-
assert d['rollouts']['public']==2
|
| 52 |
-
assert d['rollouts']['private']==1
|
| 53 |
-
assert len(d['live'])==1
|
| 54 |
-
assert dict(d['rollouts']['by_runner'])=={'harbor':4,'mimo':2,'nemo-gym':1}
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
def test_operations_show_expired_checks_without_counting_them_as_ready(data,monkeypatch):
|
| 58 |
-
monkeypatch.setattr(auto_index,'status',lambda:{'state':'indexing','next_at':time.time()-5000})
|
| 59 |
-
monkeypatch.setattr(catalog,'hidden',lambda:{'space:org/nemo'})
|
| 60 |
-
d=admin._operations(data[0])
|
| 61 |
-
assert d['indexer']['stale']
|
| 62 |
-
assert d['checks']['ready']==2
|
| 63 |
-
assert d['checks']['statuses']['Check expired']==1
|
| 64 |
-
assert d['task_catalog']['tasks']==50 # hidden and expired Spaces excluded
|
| 65 |
-
assert d['checks']['versions']==[('0.3.0',1)]
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def test_admin_runner_visibility_filters_and_safe_pagination(data):
|
| 69 |
-
d=client.get('/api/admin/rollouts?runner=nemo-gym&visibility=community').json()
|
| 70 |
-
assert d['total']==1 and d['runs'][0]['runner']=='nemo-gym'
|
| 71 |
-
assert d['runs'][0]['community']
|
| 72 |
-
assert d['runners']==['harbor','mimo','nemo-gym']
|
| 73 |
-
assert client.get('/api/admin/rollouts?visibility=community').json()['total']==2
|
| 74 |
-
assert client.get('/api/admin/rollouts?visibility=moderated').json()['total']==1
|
| 75 |
-
assert client.get('/api/admin/rollouts?visibility=private').json()['total']==1
|
| 76 |
-
assert client.get('/api/admin/rollouts?offset=-8&limit=1').json()['runs'][0]['id']=='harbor'
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def test_people_do_not_treat_reward_above_one_as_universal_success(data):
|
| 80 |
-
d=client.get('/api/admin/people').json()['people'][0]
|
| 81 |
-
assert 'solved' not in d
|
| 82 |
-
assert d['scored']==5 and d['public']==2
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
def test_environment_admin_keeps_unverified_candidates_for_review(data):
|
| 86 |
-
rows=client.get('/api/admin/environments').json()['environments']
|
| 87 |
-
assert len(rows)==4
|
| 88 |
-
assert {r['id'] for r in rows if r['ready']}=={'org/harbor','org/openenv','org/nemo'}
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def test_public_eligibility_reused_for_admin_and_community(data):
|
| 92 |
-
hidden={'hidden'}
|
| 93 |
-
for r in data[1]:
|
| 94 |
-
assert admin._admin_view(r)['community']==community.is_public(r,hidden)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_agents_install.py
DELETED
|
@@ -1,60 +0,0 @@
|
|
| 1 |
-
"""How agents get into a task's sandbox: OpenCode from its release binary, Harbor's npm install only as the fallback."""
|
| 2 |
-
|
| 3 |
-
from __future__ import annotations
|
| 4 |
-
|
| 5 |
-
import asyncio
|
| 6 |
-
from types import SimpleNamespace
|
| 7 |
-
|
| 8 |
-
from app import runner
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
class FakeEnv:
|
| 12 |
-
def __init__(self, ok: bool):
|
| 13 |
-
self.ok, self.calls = ok, []
|
| 14 |
-
|
| 15 |
-
async def exec(self, command, user=None, env=None, cwd=None, timeout_sec=None, **_):
|
| 16 |
-
self.calls.append((command, user))
|
| 17 |
-
install = "releases/download" in command
|
| 18 |
-
return SimpleNamespace(return_code=0 if (self.ok or not install) else 1, stdout="", stderr="no route")
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def _agent(tmp_path):
|
| 22 |
-
from harbor.agents.installed.opencode import OpenCode
|
| 23 |
-
|
| 24 |
-
return OpenCode(logs_dir=tmp_path, model_name="p/m")
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def test_opencode_installs_from_the_release_binary(tmp_path):
|
| 28 |
-
runner._patch_opencode_install()
|
| 29 |
-
from harbor.agents.installed.opencode import OpenCode
|
| 30 |
-
|
| 31 |
-
env = FakeEnv(ok=True)
|
| 32 |
-
asyncio.run(OpenCode.install(_agent(tmp_path), env))
|
| 33 |
-
assert "releases/download" in env.calls[0][0] and env.calls[0][1] == "root"
|
| 34 |
-
assert not any("npm i -g" in c for c, _ in env.calls) # no apt, nvm or npm
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
def test_opencode_falls_back_to_harbors_install(monkeypatch, tmp_path):
|
| 38 |
-
runner._patch_opencode_install()
|
| 39 |
-
from harbor.agents.installed.opencode import OpenCode
|
| 40 |
-
|
| 41 |
-
fell_back = []
|
| 42 |
-
monkeypatch.setattr(runner, "log", SimpleNamespace(warning=lambda *a, **k: fell_back.append(a)))
|
| 43 |
-
env = FakeEnv(ok=False)
|
| 44 |
-
try:
|
| 45 |
-
asyncio.run(OpenCode.install(_agent(tmp_path), env))
|
| 46 |
-
except Exception: # noqa: BLE001 - Harbor's own install against a fake sandbox: only that it was tried matters
|
| 47 |
-
pass
|
| 48 |
-
assert fell_back
|
| 49 |
-
assert len(env.calls) > 1 # Harbor's install ran after the binary failed
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def test_a_failed_agent_reads_as_what_went_wrong_not_its_command():
|
| 53 |
-
out = ('RuntimeError: Command failed (exit 1): opencode run -- \'Prepare the 401(k) note for Sarah ...\' | tee x\n'
|
| 54 |
-
'stdout: {"type":"error","error":{"name":"APIError","data":{"message":"Gateway Time-out","statusCode":504,'
|
| 55 |
-
'"metadata":{"url":"https://abc.gradio.live/v1/chat/completions"}}}}\n\nstderr: None')
|
| 56 |
-
msg = runner._friendly_text(out)
|
| 57 |
-
assert msg.startswith("The agent's model calls failed with Gateway Time-out (504) at the gradio.live tunnel")
|
| 58 |
-
assert "401(k)" not in msg and "Sign in again" not in msg # the prompt's "401" is not an auth failure
|
| 59 |
-
other = runner._friendly_text("RuntimeError: Command failed (exit 2): python run.py --prompt 'the whole task'\nstdout: boom")
|
| 60 |
-
assert "the whole task" not in other and "exit 2" in other and "boom" in other
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_audit_regressions.py
DELETED
|
@@ -1,163 +0,0 @@
|
|
| 1 |
-
"""Security and lifecycle regressions found during PR #33's explorer audit."""
|
| 2 |
-
import gzip
|
| 3 |
-
|
| 4 |
-
import httpx
|
| 5 |
-
import pytest
|
| 6 |
-
from fastapi.testclient import TestClient
|
| 7 |
-
|
| 8 |
-
from app import config, main, runner, spaces_live as live
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def test_forwarded_addresses_require_a_trusted_proxy(monkeypatch):
|
| 12 |
-
from starlette.requests import Request
|
| 13 |
-
from app.http import client_ip
|
| 14 |
-
request = Request({"type": "http", "headers": [(b"x-forwarded-for", b"forged, proxy-appended")], "client": ("peer", 123)})
|
| 15 |
-
monkeypatch.setattr(config, "TRUST_PROXY", False)
|
| 16 |
-
assert client_ip(request) == "peer"
|
| 17 |
-
monkeypatch.setattr(config, "TRUST_PROXY", True)
|
| 18 |
-
assert client_ip(request) == "proxy-appended"
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def test_streamed_gzip_has_a_decompression_output_cap(monkeypatch):
|
| 22 |
-
monkeypatch.setattr(live, "MAX_REPLY", 1024)
|
| 23 |
-
class Stream(httpx.SyncByteStream):
|
| 24 |
-
def __iter__(self):
|
| 25 |
-
yield gzip.compress(b"x" * 10_000_000)
|
| 26 |
-
with live.SpaceClient(transport=httpx.MockTransport(lambda r: httpx.Response(200, headers={"content-encoding": "gzip"}, stream=Stream()))) as c:
|
| 27 |
-
with pytest.raises(live.SpaceError, match="too large"):
|
| 28 |
-
c.get("https://fixture.hf.space/")
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
@pytest.mark.parametrize("headers", [
|
| 32 |
-
{"Origin": "https://evil.example", "X-Forwarded-Host": "evil.example"},
|
| 33 |
-
{"Origin": "http://testserver"},
|
| 34 |
-
{"Origin": "null"},
|
| 35 |
-
{"Origin": "https://testserver/path"},
|
| 36 |
-
{"Origin": "https://testserver@evil.example"},
|
| 37 |
-
])
|
| 38 |
-
def test_origin_must_match_scheme_and_authority(monkeypatch, headers):
|
| 39 |
-
monkeypatch.setattr(config, "PUBLIC_URL", "")
|
| 40 |
-
with TestClient(main.app, base_url="https://testserver") as c:
|
| 41 |
-
assert c.post("/api/logout", headers=headers).status_code == 403
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def test_valid_origin_and_explicit_public_origin(monkeypatch):
|
| 45 |
-
with TestClient(main.app, base_url="https://testserver") as c:
|
| 46 |
-
monkeypatch.setattr(config, "PUBLIC_URL", "")
|
| 47 |
-
assert c.post("/api/logout", headers={"Origin": "https://testserver:443"}).status_code == 200
|
| 48 |
-
monkeypatch.setattr(config, "PUBLIC_URL", "https://public.hf.space")
|
| 49 |
-
assert c.post("/api/logout", headers={"Origin": "https://public.hf.space"}).status_code == 200
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def test_personal_api_responses_are_not_cached():
|
| 53 |
-
with TestClient(main.app, base_url="https://testserver") as c:
|
| 54 |
-
assert c.get("/api/runs").headers["cache-control"] == "private, no-store"
|
| 55 |
-
assert c.get("/api/me").headers["cache-control"] == "private, no-store"
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
@pytest.mark.parametrize("bad", [".", "..", "x\n", "x/y", "%2e%2e", "//elsewhere"])
|
| 59 |
-
def test_path_parameters_cannot_change_the_published_route(bad):
|
| 60 |
-
info = {"api": [{"method": "GET", "path": "/tasks/{id}", "params": ["id"]}]}
|
| 61 |
-
with pytest.raises(live.SpaceError):
|
| 62 |
-
live.route_url(info, "GET", "/tasks/{id}", {"id": bad})
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def test_upstream_body_is_stopped_before_it_is_fully_buffered(monkeypatch):
|
| 66 |
-
monkeypatch.setattr(live, "MAX_REPLY", 64 * 1024)
|
| 67 |
-
reads = []
|
| 68 |
-
|
| 69 |
-
class Stream(httpx.SyncByteStream):
|
| 70 |
-
def __iter__(self):
|
| 71 |
-
for i in range(100):
|
| 72 |
-
reads.append(i)
|
| 73 |
-
yield b"x" * 64 * 1024
|
| 74 |
-
|
| 75 |
-
with live.SpaceClient(transport=httpx.MockTransport(lambda r: httpx.Response(200, stream=Stream()))) as c:
|
| 76 |
-
with pytest.raises(live.SpaceError, match="too large"):
|
| 77 |
-
c.get("https://fixture.hf.space/")
|
| 78 |
-
assert len(reads) <= 3
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def test_gzip_reply_is_decoded_once_and_limited(monkeypatch):
|
| 82 |
-
monkeypatch.setattr(live, "MAX_REPLY", 1024)
|
| 83 |
-
def response(r):
|
| 84 |
-
return httpx.Response(200, content=gzip.compress(b'{"ok":true}'), headers={"Content-Encoding": "gzip"})
|
| 85 |
-
with live.SpaceClient(transport=httpx.MockTransport(response)) as c:
|
| 86 |
-
assert c.get("https://fixture.hf.space/").json() == {"ok": True}
|
| 87 |
-
with live.SpaceClient(transport=httpx.MockTransport(lambda r: httpx.Response(200, content=gzip.compress(b"x" * 2048), headers={"Content-Encoding": "gzip"}))) as c:
|
| 88 |
-
with pytest.raises(live.SpaceError, match="too large"):
|
| 89 |
-
c.get("https://fixture.hf.space/")
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def test_disconnected_episode_is_not_silently_replaced(monkeypatch):
|
| 93 |
-
s = live.Session("fixture/env", "owner")
|
| 94 |
-
class Socket:
|
| 95 |
-
def send(self, message): pass
|
| 96 |
-
def recv(self, timeout): raise TimeoutError
|
| 97 |
-
def close(self): pass
|
| 98 |
-
s.ws = Socket()
|
| 99 |
-
with pytest.raises(live.SpaceError) as error:
|
| 100 |
-
live._ws_send(s, {"type": "step", "data": {}}, 1)
|
| 101 |
-
assert error.value.status == 504
|
| 102 |
-
monkeypatch.setattr(live, "_open", lambda s: pytest.fail("must not create a replacement episode"))
|
| 103 |
-
with pytest.raises(live.SpaceError) as error:
|
| 104 |
-
live._ws_send(s, {"type": "step", "data": {}}, 1)
|
| 105 |
-
assert error.value.status == 410
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
@pytest.mark.parametrize("value", [float("nan"), float("inf"), -float("inf"), 1.5])
|
| 109 |
-
def test_rollout_limits_reject_non_finite_and_fractional_values(value):
|
| 110 |
-
with pytest.raises(ValueError):
|
| 111 |
-
runner._check_fields([{"key": "steps", "type": "number", "step": 1, "min": 1, "max": 100}], {"steps": value})
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def test_raw_file_names_are_unicode_safe_and_cannot_inject_headers(monkeypatch):
|
| 115 |
-
monkeypatch.setattr(main.registry, "raw", lambda *a: (b"hello", "text/plain"))
|
| 116 |
-
c = TestClient(main.app, base_url="https://testserver")
|
| 117 |
-
for name in ['文件.txt', 'x\r\nInjected: true.txt']:
|
| 118 |
-
r = c.get('/api/env/org/ds/raw', params={"ref": "train/0", "f": name, "download": "true"})
|
| 119 |
-
assert r.status_code == 200
|
| 120 |
-
assert r.headers['content-disposition'].startswith("attachment; filename*=UTF-8''")
|
| 121 |
-
assert '\r' not in r.headers['content-disposition'] and '\n' not in r.headers['content-disposition']
|
| 122 |
-
assert 'Injected' not in r.headers
|
| 123 |
-
assert r.headers['content-security-policy'] == 'sandbox'
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
def test_query_parameters_survive_route_discovery_and_are_encoded():
|
| 127 |
-
api = {"paths": {"/tasks/{id}": {"parameters": [{"name": "split", "in": "query", "required": True, "schema": {"type": "string"}}],
|
| 128 |
-
"get": {"parameters": [{"name": "limit", "in": "query", "schema": {"type": "integer", "default": 10}}]}}}}
|
| 129 |
-
info = {"api": live._api_routes(api, False)}
|
| 130 |
-
schema = info["api"][0]["query_schema"]
|
| 131 |
-
assert schema["required"] == ["split"] and schema["properties"]["limit"]["default"] == 10
|
| 132 |
-
assert live.route_url(info, "GET", "/tasks/{id}", {"id": "a"}, {"split": "x&admin=true"}) == '/tasks/a?split=x%26admin%3Dtrue'
|
| 133 |
-
with pytest.raises(live.SpaceError, match='required'):
|
| 134 |
-
live.route_url(info, "GET", "/tasks/{id}", {"id": "a"})
|
| 135 |
-
with pytest.raises(live.SpaceError, match='published'):
|
| 136 |
-
live.route_url(info, "GET", "/tasks/{id}", {"id": "a"}, {"url": "https://evil.example"})
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
def test_mcp_routes_separate_body_path_and_query_and_keep_grading_routes(monkeypatch):
|
| 140 |
-
from app import mcp_bridge
|
| 141 |
-
api = {"paths": {"/verify/{id}": {"post": {
|
| 142 |
-
"parameters": [{"name": "id", "in": "query", "schema": {"type": "string"}}],
|
| 143 |
-
"requestBody": {"required": True, "content": {"application/json": {"schema": {"type": "object", "properties": {"id": {"type": "integer"}}, "required": ["id"]}}}},
|
| 144 |
-
}}}}
|
| 145 |
-
info = {"api": live._api_routes(api, False), "mcp": [{"name": "look", "inputSchema": {"type": "object"}}]}
|
| 146 |
-
tools = {t['name']: t for t in mcp_bridge._tools(info)}
|
| 147 |
-
assert {'look', 'verify_id'} == set(tools)
|
| 148 |
-
assert set(tools['verify_id']['inputSchema']['properties']) == {'path_params', 'query', 'body'}
|
| 149 |
-
captured = []
|
| 150 |
-
monkeypatch.setattr(mcp_bridge, '_play', lambda *a: 'session')
|
| 151 |
-
monkeypatch.setattr(live, 'act', lambda *args: captured.append(args[-1]) or {'result': {'json': {'reward': 1}, 'status': 200}})
|
| 152 |
-
args = {"path_params": {"id": "path-id"}, "query": {"id": "query-id"}, "body": {"id": 42}}
|
| 153 |
-
mcp_bridge._call('o/s', info, {"owner": "owner"}, 'verify_id', args)
|
| 154 |
-
assert captured[0]['params'] == args['path_params'] and captured[0]['body'] == args['body'] and captured[0]['query'] == args['query']
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
def test_mcp_rate_limiter_does_not_accumulate_rejected_requests():
|
| 158 |
-
from app import mcp_bridge
|
| 159 |
-
owner = 'rate-regression'
|
| 160 |
-
for _ in range(mcp_bridge.RATE[0] * 3):
|
| 161 |
-
mcp_bridge._limited(owner)
|
| 162 |
-
assert len(mcp_bridge._hits[owner]) == mcp_bridge.RATE[0]
|
| 163 |
-
mcp_bridge._hits.pop(owner)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_auto_index.py
DELETED
|
@@ -1,86 +0,0 @@
|
|
| 1 |
-
import gzip
|
| 2 |
-
import json
|
| 3 |
-
import time
|
| 4 |
-
|
| 5 |
-
import pytest
|
| 6 |
-
|
| 7 |
-
from app import auto_index, config, indexer, snapshot
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
@pytest.fixture(autouse=True)
|
| 11 |
-
def storage(tmp_path, monkeypatch):
|
| 12 |
-
monkeypatch.setattr(config, "STORAGE_DIR", tmp_path)
|
| 13 |
-
monkeypatch.setattr(config, "CACHE_DIR", tmp_path / "cache")
|
| 14 |
-
monkeypatch.setenv("RLX_SNAPSHOT_STORE", str(tmp_path))
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
def test_recent_full_listing_is_reused_without_extending_discovery_age(monkeypatch):
|
| 18 |
-
at = time.time() - 300
|
| 19 |
-
rows = [{"id": "owner/env", "kind": "dataset"}]
|
| 20 |
-
(config.STORAGE_DIR / "listing.json.gz").write_bytes(gzip.compress(json.dumps({"at": at, "full": True, "rows": rows}).encode()))
|
| 21 |
-
called = []
|
| 22 |
-
def run(store, **kwargs):
|
| 23 |
-
called.append(kwargs)
|
| 24 |
-
return {"counts": {"envs": 1, "tasks": 2}}
|
| 25 |
-
monkeypatch.setattr(indexer, "run", run)
|
| 26 |
-
auto_index.cycle()
|
| 27 |
-
assert called == [{"rows": rows, "listing_at": at, "budget_s": 90, "max_builds": 4}]
|
| 28 |
-
assert auto_index.status()["state"] == "idle"
|
| 29 |
-
assert auto_index.status()["last_success"] >= at
|
| 30 |
-
auto_index.cycle()
|
| 31 |
-
assert len(called) == 1 # app restarts and duplicate workers honor cadence
|
| 32 |
-
assert auto_index.cached_listing(at + 3601) == (None, None)
|
| 33 |
-
assert auto_index.cached_listing(at - 1) == (None, None)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def test_locked_worker_cannot_publish_duplicate_snapshot(monkeypatch):
|
| 37 |
-
fcntl = pytest.importorskip("fcntl")
|
| 38 |
-
config.CACHE_DIR.mkdir()
|
| 39 |
-
monkeypatch.setattr(indexer, "run", lambda *a, **k: pytest.fail("duplicate indexer"))
|
| 40 |
-
with (config.CACHE_DIR / "auto-index.lock").open("a") as lease:
|
| 41 |
-
fcntl.flock(lease, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
| 42 |
-
auto_index.cycle()
|
| 43 |
-
assert auto_index.status() == {}
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def test_failure_preserves_published_pointer_and_backs_off(monkeypatch):
|
| 47 |
-
store = snapshot.open_store()
|
| 48 |
-
store.put_bytes(snapshot.POINTER, b"last-good-snapshot")
|
| 49 |
-
def fail(*a, **k):
|
| 50 |
-
raise ConnectionError("upstream response must not enter public status")
|
| 51 |
-
monkeypatch.setattr(indexer, "run", fail)
|
| 52 |
-
auto_index.cycle()
|
| 53 |
-
first = auto_index.status()
|
| 54 |
-
assert first["state"] == "retrying" and first["error"] == "ConnectionError"
|
| 55 |
-
assert first["next_at"] > time.time() + 590
|
| 56 |
-
assert store.read_bytes(snapshot.POINTER) == b"last-good-snapshot"
|
| 57 |
-
auto_index._save({**first, "next_at": 0})
|
| 58 |
-
auto_index.cycle()
|
| 59 |
-
assert auto_index.status()["failures"] == 2
|
| 60 |
-
assert auto_index.status()["next_at"] > time.time() + 1190
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def test_missing_or_partial_listing_requires_fresh_discovery():
|
| 64 |
-
assert auto_index.cached_listing(time.time()) == (None, None)
|
| 65 |
-
doc = {"at": time.time(), "full": False, "rows": [{"id": "owner/partial"}]}
|
| 66 |
-
(config.STORAGE_DIR / "listing.json.gz").write_bytes(gzip.compress(json.dumps(doc).encode()))
|
| 67 |
-
assert auto_index.cached_listing(time.time()) == (None, None)
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def test_shutdown_terminates_only_owned_indexer_child(monkeypatch):
|
| 71 |
-
calls = []
|
| 72 |
-
class Child:
|
| 73 |
-
def poll(self): return None
|
| 74 |
-
def terminate(self): calls.append("terminate")
|
| 75 |
-
def wait(self, **kwargs): calls.append("wait")
|
| 76 |
-
def popen(command, **kwargs):
|
| 77 |
-
assert command[-2:] == ["-m", "app.auto_index"]
|
| 78 |
-
assert kwargs["env"]["HF_HUB_DISABLE_IMPLICIT_TOKEN"] == "1"
|
| 79 |
-
return Child()
|
| 80 |
-
monkeypatch.setattr(auto_index.subprocess, "Popen", popen)
|
| 81 |
-
auto_index._stop.set()
|
| 82 |
-
try:
|
| 83 |
-
auto_index._child()
|
| 84 |
-
finally:
|
| 85 |
-
auto_index._stop.clear()
|
| 86 |
-
assert calls == ["terminate", "wait"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_capabilities.py
DELETED
|
@@ -1,68 +0,0 @@
|
|
| 1 |
-
"""Support claims reflect runtime evidence, not a source's framework tag."""
|
| 2 |
-
import pytest
|
| 3 |
-
|
| 4 |
-
from app import spaces_live as live
|
| 5 |
-
from app.envs.capabilities import dataset_support, space_support
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
def state(support, feature):
|
| 9 |
-
return next(c["state"] for c in support["capabilities"] if c["id"] == feature)
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
@pytest.mark.parametrize("framework,runner", [("Verifiers", "verifiers"), ("NeMo Gym", "nemo-gym"), ("verl", "verl"), ("SkyRL", "skyrl")])
|
| 13 |
-
def test_native_framework_dataset_does_not_claim_local_execution(framework, runner):
|
| 14 |
-
support = dataset_support(framework, options=[{"runner": runner, "label": framework, "ok": False, "why": "Use its native harness"}])
|
| 15 |
-
assert state(support, "browse") == "available"
|
| 16 |
-
assert state(support, "run") == "external"
|
| 17 |
-
assert support["framework"]["docs"].startswith("https://")
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def test_task_support_distinguishes_blocked_from_runnable():
|
| 21 |
-
options = [{"runner": "harbor", "label": "Harbor", "ok": False, "why": "GPU required"}]
|
| 22 |
-
assert state(dataset_support("Harbor", options=options), "run") == "unavailable"
|
| 23 |
-
options[0]["ok"] = True
|
| 24 |
-
assert state(dataset_support("Harbor", options=options), "run") == "available"
|
| 25 |
-
assert state(dataset_support("Harbor"), "run") == "unknown"
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def test_sleeping_server_never_claims_live_capabilities():
|
| 29 |
-
s = space_support({"running": False, "stage": "SLEEPING", "last_seen": {"step_api": True}})
|
| 30 |
-
assert state(s, "play") == "unknown"
|
| 31 |
-
assert s["transport"] is None
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
@pytest.mark.parametrize("protocol", ["openenv", "gymnasium", "nemo-gym", "mcp", "api"])
|
| 35 |
-
def test_probe_uses_published_protocol_not_reset_route_or_tag(monkeypatch, protocol):
|
| 36 |
-
from app import catalog
|
| 37 |
-
catalog._memo.clear()
|
| 38 |
-
rec = {"id": "fixture/env", "stage": "RUNNING", "host": "https://fixture-env.hf.space", "base_path": None, "tags": ["openenv"]}
|
| 39 |
-
monkeypatch.setattr(live, "record", lambda *a, **k: rec)
|
| 40 |
-
paths = {"/reset": {"post": {}}, "/step": {"post": {}}} if protocol in {"openenv", "gymnasium"} else (
|
| 41 |
-
{"/seed_session": {"post": {}}, "/verify": {"post": {}}} if protocol == "nemo-gym" else {"/mcp": {"post": {}}} if protocol == "mcp" else {"/guess": {"post": {}}})
|
| 42 |
-
docs = {"/openapi.json": {"info": {"version": "1"}, "paths": paths}}
|
| 43 |
-
if protocol == "openenv":
|
| 44 |
-
docs["/schema"] = {k: {"type": "object", "properties": {}} for k in ("action", "observation", "state")}
|
| 45 |
-
monkeypatch.setattr(live, "_get_json", lambda rec, path: docs.get(path))
|
| 46 |
-
monkeypatch.setattr(live, "_ui_path", lambda rec: None)
|
| 47 |
-
monkeypatch.setattr(live, "_tools", lambda rec: ([{"name": "guess"}] if protocol == "mcp" else [], None, {}))
|
| 48 |
-
monkeypatch.setattr(live, "remember", lambda *a: None)
|
| 49 |
-
info = live.probe("fixture/env")
|
| 50 |
-
assert info["framework"] == protocol
|
| 51 |
-
assert info["openenv"] is (protocol == "openenv")
|
| 52 |
-
assert info["step_api"] is (protocol == "openenv")
|
| 53 |
-
if protocol == "gymnasium":
|
| 54 |
-
assert {r["path"] for r in info["api"]} == {"/reset", "/step"}
|
| 55 |
-
assert info["support"]["transport"] == "http"
|
| 56 |
-
assert state(info["support"], "play") == "available"
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def test_private_dataset_does_not_offer_public_mcp():
|
| 60 |
-
assert state(dataset_support("Harbor", restricted=True), "mcp") == "unavailable"
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def test_packed_harbor_rows_keep_the_harbor_lifecycle():
|
| 64 |
-
assert dataset_support("Harbor (packed)")["framework"]["id"] == "harbor"
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def test_unknown_runner_is_not_claimed_as_hosted():
|
| 68 |
-
assert state(dataset_support("Custom", options=[{"runner": "custom", "label": "Custom", "ok": True}]), "run") == "unavailable"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_community.py
DELETED
|
@@ -1,96 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
|
| 3 |
-
import pytest
|
| 4 |
-
from fastapi.testclient import TestClient
|
| 5 |
-
|
| 6 |
-
from app import auth, community, main, settings, store
|
| 7 |
-
|
| 8 |
-
client = TestClient(main.app)
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
def record(id, runner="harbor", **fields):
|
| 12 |
-
return {"id": id, "dataset": f"org/{runner}", "path": "task/1", "title": "A task", "user": "hidden-user",
|
| 13 |
-
"runner": runner, "status": "done", "reward": 1, "visibility": "public", "model": "org/model",
|
| 14 |
-
"created_at": 10, **fields}
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
@pytest.fixture
|
| 18 |
-
def records(monkeypatch):
|
| 19 |
-
rows = [record("harbor", reward=-3, dataset="a/same"), record("mimo", "mimo", domain="code", reward=0.25),
|
| 20 |
-
record("nemo", "nemo-gym", reward=1, dataset="b/same"), record("future", "custom-runtime", reward=20),
|
| 21 |
-
record("private", visibility="private"), record("restricted", restricted=True),
|
| 22 |
-
record("failed", status="failed", reward=None), record("hidden")]
|
| 23 |
-
monkeypatch.setattr(store, "list_runs", lambda **kw: rows)
|
| 24 |
-
monkeypatch.setattr(store, "has_artifact", lambda *a: False)
|
| 25 |
-
monkeypatch.setattr(settings, "get", lambda key, default=None: ["hidden"] if key == "hidden_runs" else default)
|
| 26 |
-
return rows
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def test_all_saved_runners_share_the_feed_with_scoped_counts(records):
|
| 30 |
-
data = client.get("/api/community").json()
|
| 31 |
-
assert {r["id"] for r in data["runs"]} == {"harbor", "mimo", "nemo", "future"}
|
| 32 |
-
assert data["total"] == data["stats"]["runs"] == 4
|
| 33 |
-
assert data["stats"]["datasets"] == data["stats"]["runners"] == 4
|
| 34 |
-
assert not data["stats"]["comparable"]
|
| 35 |
-
assert "mean" not in data["stats"] # no meaningless global reward average
|
| 36 |
-
assert {r["group"] for r in data["runs"]} >= {"a/same", "b/same"}
|
| 37 |
-
assert "hidden-user" not in json.dumps(data)
|
| 38 |
-
for runner in ("harbor", "mimo", "nemo-gym", "custom-runtime"):
|
| 39 |
-
subset = client.get("/api/community", params={"runner": runner}).json()
|
| 40 |
-
assert subset["total"] == subset["stats"]["runs"] == subset["stats"]["datasets"] == 1
|
| 41 |
-
assert {r["runner"] for r in subset["runs"]} == {runner}
|
| 42 |
-
assert len(subset["facets"]["runner"]) == 4 # own facet retains alternatives
|
| 43 |
-
assert len(subset["facets"]["dataset"]) == 1
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
def test_search_includes_environment_runner_and_model_and_empty_counts(records):
|
| 47 |
-
for query, expected in (("b/same", "nemo"), ("nemo-gym", "nemo"), ("org/mimo model", "mimo")):
|
| 48 |
-
data = client.get("/api/community", params={"q": query}).json()
|
| 49 |
-
assert [r["id"] for r in data["runs"]] == [expected]
|
| 50 |
-
empty = client.get("/api/community?dataset=nobody/missing").json()
|
| 51 |
-
assert empty["total"] == empty["stats"]["datasets"] == empty["stats"]["models"] == empty["stats"]["runners"] == 0
|
| 52 |
-
assert empty["stats"]["results"] == []
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def test_reward_summaries_separate_metrics_domains_judges_and_revisions():
|
| 56 |
-
rows = [record("a", reward=-4), record("b", reward=-2)]
|
| 57 |
-
for field, value in (("reward_key", "latency"), ("domain", "code"), ("judge", "judge/new"), ("sha", "new-revision")):
|
| 58 |
-
rows.append(record(field, reward=100, **{field: value}))
|
| 59 |
-
data = community.stats(rows)
|
| 60 |
-
assert len(data["results"]) == 5
|
| 61 |
-
original = next(r for r in data["results"] if r["runs"] == 2)
|
| 62 |
-
assert original["mean"] == -3 and original["max"] == -2
|
| 63 |
-
assert community.stats([record("a", reward=1e308), record("b", reward=1e308)])["results"][0]["mean"] == 1e308
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
@pytest.mark.parametrize("value", [None, "1", True, {}, float("nan"), float("inf"), 10**1000])
|
| 67 |
-
def test_invalid_rewards_cannot_break_or_enter_public_feed(value, monkeypatch):
|
| 68 |
-
monkeypatch.setattr(settings, "get", lambda *a: [])
|
| 69 |
-
assert not main._shareable(record("bad", reward=value))
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
def test_pagination_sort_and_negative_reward_band(records):
|
| 73 |
-
records.extend(record(f"r{i:02}", created_at=i, reward=-i, cost={"total": None}) for i in range(20))
|
| 74 |
-
all_rows = client.get("/api/community?sort=reward_asc").json()["runs"]
|
| 75 |
-
pages = [client.get("/api/community", params={"sort": "reward_asc", "offset": i, "limit": 5}).json()["runs"] for i in range(0, 24, 5)]
|
| 76 |
-
assert [r["id"] for page in pages for r in page] == [r["id"] for r in all_rows]
|
| 77 |
-
negative = client.get("/api/community?reward=zero").json()
|
| 78 |
-
assert negative["total"] == 21 and all(r["reward"] <= 0 for r in negative["runs"])
|
| 79 |
-
assert client.get("/api/community?sort=cost_asc").status_code == 200
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
@pytest.mark.parametrize("runner,domain,events", [("harbor", None, False), ("mimo", "code", True), ("nemo-gym", "nemo-gym", True), (None, "code", True)])
|
| 83 |
-
def test_trace_routes_match_runner_and_keep_public_scrubbing(monkeypatch, runner, domain, events):
|
| 84 |
-
from app.mimo.runner import core
|
| 85 |
-
r = record("trace", runner, domain=domain)
|
| 86 |
-
monkeypatch.setattr(store, "get", lambda id: r)
|
| 87 |
-
monkeypatch.setattr(settings, "get", lambda *a: [])
|
| 88 |
-
monkeypatch.setattr(auth, "current_user", lambda request: None)
|
| 89 |
-
monkeypatch.setattr(store, "read_artifact", lambda *a: b'[{"content":"hidden-user"}]')
|
| 90 |
-
monkeypatch.setattr(main.runner, "is_live", lambda *a: False)
|
| 91 |
-
monkeypatch.setattr(core, "live", lambda *a: None)
|
| 92 |
-
monkeypatch.setattr(core, "events", lambda *a: [{"content": "hidden-user"}])
|
| 93 |
-
response = client.get("/api/runs/trace")
|
| 94 |
-
assert response.status_code == 200 and "hidden-user" not in response.text
|
| 95 |
-
assert ("events" in response.json()) == events
|
| 96 |
-
assert response.json()["run"]["runner"] == (runner or "mimo")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_contract.py
DELETED
|
@@ -1,175 +0,0 @@
|
|
| 1 |
-
"""The environment contract (app/envs/contract.py): a new format plugs in by registering an adapter, and then the
|
| 2 |
-
pages' API, the runner, rollouts across a task's forms and the MCP server all work for it, with nothing else changed
|
| 3 |
-
(no network: the Hub is stubbed).
|
| 4 |
-
|
| 5 |
-
What is checked: the surest adapter that confirms reads a source, and one that hands a source over isn't kept;
|
| 6 |
-
views come back without an adapter's private keys and with run options and aliases; files and folders go through the
|
| 7 |
-
adapter; a run is refused unless the task's chosen run option says it can run, with its own inputs checked and its
|
| 8 |
-
agents limited; rollouts of the same task elsewhere are listed with it; linkers add links; the dataset MCP server
|
| 9 |
-
lists, reads and withholds through the same adapter; row refs round-trip.
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
from __future__ import annotations
|
| 13 |
-
|
| 14 |
-
import json
|
| 15 |
-
import time
|
| 16 |
-
from pathlib import Path
|
| 17 |
-
from types import SimpleNamespace
|
| 18 |
-
|
| 19 |
-
import pytest
|
| 20 |
-
from fastapi.testclient import TestClient
|
| 21 |
-
|
| 22 |
-
from app import auth, main, models, runner, settings, store
|
| 23 |
-
from app.envs import contract as c, registry, rows
|
| 24 |
-
|
| 25 |
-
client = TestClient(main.app, base_url="https://testserver")
|
| 26 |
-
TOKEN = "hf_contract_secret_token_0003"
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def cookie() -> dict[str, str]:
|
| 30 |
-
session = {"token": TOKEN, "name": "carol", "avatar": None, "via": "token", "exp": time.time() + 3600}
|
| 31 |
-
return {"Cookie": f"{auth.COOKIE}={auth._box.encrypt(json.dumps(session).encode()).decode()}"}
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
class Toy(c.Adapter):
|
| 35 |
-
"""A made-up format: tasks are numbered puzzles; puzzle 2 holds its answer in a file; "3" can't run."""
|
| 36 |
-
id, name, framework = "toy", "Toy puzzles", "Toy"
|
| 37 |
-
about = "Each task is a numbered puzzle."
|
| 38 |
-
|
| 39 |
-
def detect(self, kind, meta):
|
| 40 |
-
return 0.95 if meta["id"].startswith("toy/") else 0.0
|
| 41 |
-
|
| 42 |
-
def summary(self, env, subset=None):
|
| 43 |
-
return {"state": "ready", "total": 3, "inline": True, "search": True, "subsets": [], "how": {"framework": "Toy", "name": self.name, "about": self.about},
|
| 44 |
-
"facets": [c.facet("level", "Level", "level")], "overview": [c.section("graded", "How it's graded", [c.kv([("Reward", "1 if solved")])])]}
|
| 45 |
-
|
| 46 |
-
def tasks(self, env, *, subset=None, q="", filters=None, offset=0, everything=False):
|
| 47 |
-
cards = [c.card(f"p/{i}", f"Puzzle {i}", facets={"level": ["easy" if i < 2 else "hard"]}) for i in (1, 2, 3)]
|
| 48 |
-
return {"cards": cards, "total": 3, "offset": 0, "page": 3}
|
| 49 |
-
|
| 50 |
-
def task(self, env, ref):
|
| 51 |
-
if ref not in ("p/1", "p/2", "p/3"):
|
| 52 |
-
raise LookupError("no such puzzle")
|
| 53 |
-
return {"ref": ref, "title": f"Puzzle {ref[2:]}", "sections": [c.section("task", "The task", [c.markdown("Solve it."),
|
| 54 |
-
c.files([{"path": "README.md", "size": 3}, {"path": "answer.txt", "size": 2, "withheld": True}])])],
|
| 55 |
-
"glance": [["Level", "easy"]], "withheld": ["answer"], "links": [], "_secret": "never shown"}
|
| 56 |
-
|
| 57 |
-
def file(self, env, ref, path):
|
| 58 |
-
return {"path": path, "withheld": True} if path == "answer.txt" else {"path": path, "text": "hi\n", "size": 3}
|
| 59 |
-
|
| 60 |
-
def random(self, env, subset=None):
|
| 61 |
-
return "p/1"
|
| 62 |
-
|
| 63 |
-
def run_options(self, env, ref, view):
|
| 64 |
-
return [c.run_option("harbor", "Harbor", ok=ref != "p/3", why="no image", default=True,
|
| 65 |
-
fields=[c.field("judge", "Judge", "select", default="a", options=["a", "b"]), c.field("hint", "Hint", "bool", default=False)],
|
| 66 |
-
harnesses=["opencode"])]
|
| 67 |
-
|
| 68 |
-
def run_task(self, env, ref):
|
| 69 |
-
return {"title": f"Puzzle {ref[2:]}", "restricted": False, "sha": "abc", "bytes": 10, "runnable": {"ok": True, "how": "image"}, "image": "python:3.12"}
|
| 70 |
-
|
| 71 |
-
def aliases(self, env, ref):
|
| 72 |
-
return [("toy/converted", f"tasks/{ref[2:]}")]
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
class Unsure(c.Adapter):
|
| 76 |
-
"""Sure of everything at first, then hands every source over (a Harbor tag on a dataset of rows)."""
|
| 77 |
-
id = "unsure"
|
| 78 |
-
|
| 79 |
-
def detect(self, kind, meta):
|
| 80 |
-
return 0.99 if meta["id"].startswith("toy/") else 0.0
|
| 81 |
-
|
| 82 |
-
def confirm(self, env):
|
| 83 |
-
return False
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
@pytest.fixture(autouse=True)
|
| 87 |
-
def toy(monkeypatch):
|
| 88 |
-
monkeypatch.setattr(registry, "_meta", lambda kind, spec, token: {"id": spec, "sha": "abc", "restricted": False})
|
| 89 |
-
monkeypatch.setattr(registry, "ADAPTERS", [Unsure(), Toy(), *registry.ADAPTERS])
|
| 90 |
-
monkeypatch.setattr(registry, "_choice", {})
|
| 91 |
-
monkeypatch.setattr(models, "get", lambda mid: {"id": mid, "tools": True, "provider": "x"})
|
| 92 |
-
monkeypatch.setattr(runner.Rollout, "execute", lambda self: None)
|
| 93 |
-
monkeypatch.setattr(settings, "_org_members", lambda: set())
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def test_the_surest_adapter_that_confirms_reads_it():
|
| 97 |
-
env = registry.resolve("dataset", "toy/puzzles")
|
| 98 |
-
assert env.adapter.id == "toy"
|
| 99 |
-
d = client.get("/api/env/toy/puzzles").json()
|
| 100 |
-
assert d["env"]["adapter"] == "toy" and d["env"]["framework"] == "Toy" and d["total"] == 3 and d["inline"]
|
| 101 |
-
assert d["overview"][0]["blocks"][0] == {"type": "kv", "rows": [["Reward", "1 if solved"]]}
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
def test_task_views_are_public_and_complete():
|
| 105 |
-
v = client.get("/api/env/toy/puzzles/task?ref=p/1").json()
|
| 106 |
-
assert "_secret" not in json.dumps(v)
|
| 107 |
-
assert v["env"]["adapter"] == "toy" and v["run"]["options"][0]["ok"] and v["run"]["note"] == ""
|
| 108 |
-
assert v["aliases"] == [{"env": "toy/converted", "ref": "tasks/1"}]
|
| 109 |
-
assert client.get("/api/env/toy/puzzles/task?ref=nope").status_code == 404
|
| 110 |
-
assert client.get("/api/env/toy/puzzles/file?ref=p/2&f=answer.txt").json()["withheld"] is True
|
| 111 |
-
assert client.get("/api/env/toy/puzzles/random").json() == {"ref": "p/1"}
|
| 112 |
-
assert client.get("/api/env/toy/puzzles/folder?ref=p/1&f=x/").status_code == 404 # lists its folders up front
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
def test_linkers_add_links_without_either_adapter_knowing(monkeypatch):
|
| 116 |
-
monkeypatch.setattr(registry, "LINKERS", [lambda env, ref, view: [c.link("Raw row", "/t/raw/ds/train/1", rel="same")]])
|
| 117 |
-
v = client.get("/api/env/toy/puzzles/task?ref=p/1").json()
|
| 118 |
-
assert v["links"] == [{"label": "Raw row", "href": "/t/raw/ds/train/1", "note": "", "rel": "same"}]
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
def test_runs_follow_the_chosen_run_option():
|
| 122 |
-
post = lambda **kw: client.post("/api/runs", json={"dataset": "toy/puzzles", "path": "p/1", "model": "m/x", **kw}, headers=cookie())
|
| 123 |
-
r = post(fields={"judge": "b"})
|
| 124 |
-
assert r.status_code == 200, r.text
|
| 125 |
-
run = r.json()["run"]
|
| 126 |
-
assert run["adapter"] == "toy" and run["runner"] == "harbor" and run["fields"] == {"judge": "b", "hint": False} and run["env"] == "toy/puzzles"
|
| 127 |
-
assert TOKEN not in r.text
|
| 128 |
-
assert post(fields={"judge": "zzz"}).status_code == 400 # not one of its choices
|
| 129 |
-
assert post(fields={"other": 1}).status_code == 400 # an input it doesn't take
|
| 130 |
-
assert post(fields={"hint": "yes"}).status_code == 400 # not a bool
|
| 131 |
-
assert post(runner="mimo").status_code == 400 # no such runner for this task
|
| 132 |
-
assert post(runner="Bad Runner!").status_code == 422
|
| 133 |
-
assert post(harness="terminus-2").status_code == 400 # this runner drives only opencode
|
| 134 |
-
r = client.post("/api/runs", json={"dataset": "toy/puzzles", "path": "p/3", "model": "m/x"}, headers=cookie())
|
| 135 |
-
assert r.status_code == 400 and "no image" in r.json()["detail"]
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
def test_rollouts_of_the_same_task_elsewhere_are_listed_with_it():
|
| 139 |
-
base = {"user": "carol", "title": "T", "model": "m/x", "harness": "opencode", "status": "done", "reward": 1.0, "visibility": "public"}
|
| 140 |
-
here = store.create({**base, "id": "20260102-000000-aaaaaa", "dataset": "toy/puzzles", "path": "p/2", "task_id": "toy/puzzles:p/2"})
|
| 141 |
-
there = store.create({**base, "id": "20260102-000000-bbbbbb", "dataset": "toy/converted", "path": "tasks/2", "task_id": "toy/converted:tasks/2", "user": "dave"})
|
| 142 |
-
other = store.create({**base, "id": "20260102-000000-cccccc", "dataset": "toy/converted", "path": "tasks/3", "task_id": "toy/converted:tasks/3"})
|
| 143 |
-
d = client.get("/api/env/toy/puzzles/runs?ref=p/2", headers=cookie()).json()
|
| 144 |
-
assert [r["id"] for r in d["mine"]] == [here["id"]]
|
| 145 |
-
assert [r["id"] for r in d["public"]] == [there["id"]] and "user" not in d["public"][0]
|
| 146 |
-
assert other["id"] not in json.dumps(d)
|
| 147 |
-
assert d["elsewhere"] == [{"env": "toy/converted", "ref": "tasks/2"}]
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
def test_the_dataset_mcp_server_uses_the_same_adapter():
|
| 151 |
-
def rpc(method, params=None, **headers):
|
| 152 |
-
return client.post("/mcp/d/toy/puzzles", json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}}, headers=headers)
|
| 153 |
-
|
| 154 |
-
init = rpc("initialize", {"protocolVersion": "2025-06-18"})
|
| 155 |
-
assert init.status_code == 200 and init.headers.get("mcp-session-id") and "Toy" in init.json()["result"]["serverInfo"]["name"]
|
| 156 |
-
names = [t["name"] for t in rpc("tools/list").json()["result"]["tools"]]
|
| 157 |
-
assert names == ["describe", "list_tasks", "get_task", "read_file", "random_task"]
|
| 158 |
-
call = lambda name, args: rpc("tools/call", {"name": name, "arguments": args}).json()["result"]
|
| 159 |
-
assert json.loads(call("list_tasks", {"filters": {"level": ["hard"]}})["content"][0]["text"])["total"] == 2
|
| 160 |
-
assert json.loads(call("list_tasks", {"query": "puzzle 3"})["content"][0]["text"])["tasks"][0]["ref"] == "p/3"
|
| 161 |
-
text = call("get_task", {"ref": "p/2"})["content"][0]["text"]
|
| 162 |
-
assert "Solve it." in text and "answer.txt (withheld)" in text and "never shown" not in text
|
| 163 |
-
assert "withheld" in call("read_file", {"ref": "p/2", "path": "answer.txt"})["content"][0]["text"]
|
| 164 |
-
assert call("get_task", {"ref": "nope"})["isError"] is True
|
| 165 |
-
assert rpc("tools/list", Origin="https://evil.example").status_code == 403
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
def test_row_refs_round_trip():
|
| 169 |
-
for config, split, i in (("default", "train", 0), ("code", "train", 6), ("a/b", "test", 12)):
|
| 170 |
-
ref = rows.ref_of(config, split, i)
|
| 171 |
-
assert rows.parse_ref(ref) == (config, split, i)
|
| 172 |
-
assert rows.ref_of("default", "train", 3) == "train/3" and rows.ref_of("code", "train", 6) == "code/train/6"
|
| 173 |
-
assert rows.subset_id("default", "train") == "train" and rows.parse_subset("code/train") == ("code", "train")
|
| 174 |
-
with pytest.raises(LookupError): # a 404, not a 500
|
| 175 |
-
rows.parse_ref("train/x")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_deploy_spaces.py
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
from scripts import deploy_spaces
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
def test_upload_manifest_excludes_local_secrets_and_symlinks(tmp_path, monkeypatch):
|
| 5 |
-
for name in ('app/main.py','web/app.css','.env','.env.production','sub/.env.local','sub/private.key','.local-data/run.json','.venv/env.py'):
|
| 6 |
-
path=tmp_path/name
|
| 7 |
-
path.parent.mkdir(parents=True,exist_ok=True)
|
| 8 |
-
path.write_text('sample')
|
| 9 |
-
(tmp_path/'linked.py').symlink_to(tmp_path/'.env')
|
| 10 |
-
(tmp_path/'linked-dir').symlink_to(tmp_path/'.local-data',target_is_directory=True)
|
| 11 |
-
monkeypatch.setattr(deploy_spaces,'ROOT',tmp_path)
|
| 12 |
-
assert deploy_spaces.files()==['app/main.py','web/app.css']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_discovery.py
DELETED
|
@@ -1,69 +0,0 @@
|
|
| 1 |
-
"""Discovery evidence is metadata, not a runtime attestation or an editorial decision."""
|
| 2 |
-
from types import SimpleNamespace
|
| 3 |
-
|
| 4 |
-
import pytest
|
| 5 |
-
from huggingface_hub import SpaceInfo
|
| 6 |
-
|
| 7 |
-
from app import catalog
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def space(sid="org/env", *, tags=(), files=(), **kw):
|
| 11 |
-
return SpaceInfo(id=sid, sdk="docker", private=False, tags=list(tags), likes=0,
|
| 12 |
-
siblings=[{"rfilename": f} for f in files], **kw)
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@pytest.mark.parametrize("files", [("openenv.yaml",), ("my_env/openenv.yml",)])
|
| 16 |
-
def test_manifest_discovery(files):
|
| 17 |
-
d = catalog._space_summary(space(files=files))
|
| 18 |
-
assert d["openenv"] and d["manifest"] == files[0]
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
@pytest.mark.parametrize("file", ["src/openenv.yaml", "templates/openenv.yaml", "examples/openenv.yaml",
|
| 22 |
-
"build/openenv.yaml", "vendor/openenv.yaml", "envs/echo/openenv.yaml",
|
| 23 |
-
"src/openenv/cli/templates/openenv.yaml", ".venv/openenv.yaml"])
|
| 24 |
-
def test_vendored_manifests_are_not_declarations(file):
|
| 25 |
-
d = catalog._space_summary(space(files=[file]))
|
| 26 |
-
assert not d["openenv"] and d["manifest"] is None
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
@pytest.mark.parametrize("tag", ["openenv", "library:openenv", "openenv-0.7.0"])
|
| 30 |
-
def test_tag_only_remains_a_candidate_without_a_manifest(tag):
|
| 31 |
-
d = catalog._space_summary(space(tags=[tag]))
|
| 32 |
-
assert d["openenv"] and d["manifest"] is None
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def test_names_and_generic_rl_mcp_tags_do_not_establish_openenv():
|
| 36 |
-
d = catalog._space_summary(space("openenv/not-an-env", tags=["rl-environment", "mcp-server"]))
|
| 37 |
-
assert not d["openenv"] and d["mcp"]
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def test_space_census_deduplicates_and_applies_visibility_to_featured(monkeypatch):
|
| 41 |
-
good = space(tags=["openenv"])
|
| 42 |
-
article = space("org/article", tags=["openenv", "research-article-template"])
|
| 43 |
-
private = space("org/private"); private.private = True
|
| 44 |
-
static = space("org/static"); static.sdk = "static"
|
| 45 |
-
by = {s.id: s for s in [good, article, private, static]}
|
| 46 |
-
monkeypatch.setattr(catalog, "collections", lambda: [{"ids": [f"space:{s}" for s in by]}])
|
| 47 |
-
monkeypatch.setattr(catalog, "_api", lambda: SimpleNamespace(list_spaces=lambda **kw: iter(by.values()),
|
| 48 |
-
space_info=lambda sid, **kw: by[sid]))
|
| 49 |
-
assert [r["id"] for r in catalog.spaces()] == [good.id]
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
def test_failed_full_census_is_not_silently_published(monkeypatch):
|
| 53 |
-
def listing(**kw):
|
| 54 |
-
yield space(tags=["openenv"])
|
| 55 |
-
raise ConnectionError("page two unavailable")
|
| 56 |
-
|
| 57 |
-
monkeypatch.setattr(catalog, "collections", lambda: [])
|
| 58 |
-
monkeypatch.setattr(catalog, "_api", lambda: SimpleNamespace(list_spaces=listing))
|
| 59 |
-
with pytest.raises(ConnectionError):
|
| 60 |
-
catalog.spaces(full=True)
|
| 61 |
-
assert len(catalog.spaces(full=False)) == 1 # a quick bootstrap may serve candidates while refreshing
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def test_collection_never_reclassifies_framework(monkeypatch):
|
| 65 |
-
d = catalog._space_summary(space(tags=["ors"]))
|
| 66 |
-
monkeypatch.setattr(catalog, "_listing_rows", lambda: [d])
|
| 67 |
-
monkeypatch.setattr(catalog, "collections", lambda: [{"id": "fineenvs", "ids": [d["key"]]}])
|
| 68 |
-
monkeypatch.setattr(catalog, "hidden", lambda: set())
|
| 69 |
-
assert catalog.environments()[0]["framework"] == "ors"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_envs.py
DELETED
|
@@ -1,307 +0,0 @@
|
|
| 1 |
-
"""Rows datasets (app/envs) and big Harbor datasets walked folder by folder (no network: rows and the Hub are stubbed).
|
| 2 |
-
|
| 3 |
-
What is checked: each processor claims the datasets it should; answers never come back (named columns, nested
|
| 4 |
-
keys, a packed task's solution and answer files, MiMo's patches); archives can't escape their folder or blow up;
|
| 5 |
-
filters can't inject; the direct reader pages JSON Lines right; the walker finds tasks without listing whole repos,
|
| 6 |
-
and a task's unlisted folders can't be used to reach files outside it.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
import base64
|
| 12 |
-
import io
|
| 13 |
-
import json
|
| 14 |
-
import tarfile
|
| 15 |
-
from types import SimpleNamespace
|
| 16 |
-
|
| 17 |
-
import pytest
|
| 18 |
-
from fastapi.testclient import TestClient
|
| 19 |
-
|
| 20 |
-
from app import catalog, envs, main
|
| 21 |
-
from app.envs import base, direct, processors
|
| 22 |
-
|
| 23 |
-
client = TestClient(main.app, base_url="https://testserver")
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def ds(columns, rows, tags=(), spec="org/ds"):
|
| 27 |
-
return base.Dataset(spec=spec, tags=list(tags), card={}, splits=[{"config": "default", "split": "train"}],
|
| 28 |
-
features=[{"name": c, "type": {}} for c in columns], sample=rows)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def tarball(files: dict[str, bytes], prefix="task_0/") -> str:
|
| 32 |
-
buf = io.BytesIO()
|
| 33 |
-
with tarfile.open(fileobj=buf, mode="w:gz") as t:
|
| 34 |
-
for name, data in files.items():
|
| 35 |
-
info = tarfile.TarInfo(prefix + name if not name.startswith(("..", "/")) else name)
|
| 36 |
-
info.size = len(data)
|
| 37 |
-
t.addfile(info, io.BytesIO(data))
|
| 38 |
-
return base64.b64encode(buf.getvalue()).decode()
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
TASK = {
|
| 42 |
-
"task.toml": b'[metadata]\ntitle = "Count the words"\ndifficulty = "easy"\ngold_answer = "42"\n[environment]\ndocker_image = "python:3.12"\n',
|
| 43 |
-
"instruction.md": b"# Count the words\n\nCount the words in /app/a.txt.\n",
|
| 44 |
-
"tests/test.sh": b"#!/bin/bash\nexpected=42\necho 1 > /logs/verifier/reward.txt\n",
|
| 45 |
-
"tests/expected_output.json": b'{"answer": 42}',
|
| 46 |
-
"solution/solve.sh": b"echo 42\n",
|
| 47 |
-
"task_manifest.json": b'{"original_bash": "wc -w /app/a.txt"}',
|
| 48 |
-
"environment/Dockerfile": b"FROM python:3.12\n",
|
| 49 |
-
"../escape.txt": b"nope",
|
| 50 |
-
}
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
# ── which processor reads what ───────────────────────────────────────────────
|
| 54 |
-
def test_processors_claim_their_formats():
|
| 55 |
-
pick = lambda d: envs.pick(d).id # noqa: E731
|
| 56 |
-
assert pick(ds(["path", "task_binary"], [{"path": "t", "task_binary": "x"}], tags=["library:harbor"])) == "harbor-packed"
|
| 57 |
-
assert pick(ds(["data_source", "prompt", "ability", "reward_model", "extra_info", "agent_name"], [{}], spec="XiaomiMiMo/MiMo-V2.6-RL-oss")) == "mimo"
|
| 58 |
-
assert pick(ds(["id", "responses_create_params", "kwargs"], [{}])) == "nemo-gym"
|
| 59 |
-
assert pick(ds(["data_source", "prompt", "reward_model"], [{}])) == "verl"
|
| 60 |
-
conv = [{"role": "user", "content": "do it"}, {"role": "assistant", "content": "done"}]
|
| 61 |
-
assert pick(ds(["conversations", "agent", "result"], [{"conversations": conv, "agent": "a", "result": "pass"}])) == "traces"
|
| 62 |
-
assert pick(ds(["question", "answer"], [{"question": "q", "answer": "a"}], tags=["library:verifiers"])) == "verifiers"
|
| 63 |
-
assert pick(ds(["question", "answer"], [{"question": "q", "answer": "a"}])) == "generic"
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
def test_generic_roles_and_answers():
|
| 67 |
-
rows = [{"task_id": f"t{i}", "instruction": f"You are an agent.\nWork carefully.\n\nQuestion {i}: what is {i}?", "question": f"what is {i}?",
|
| 68 |
-
"answer": str(i), "reward_mode": "exact", "atol": 0.0, "files": ["a.csv"], "difficulty": "easy"} for i in range(5)]
|
| 69 |
-
d = ds(list(rows[0]), rows)
|
| 70 |
-
p = envs.pick(d)
|
| 71 |
-
roles = p.roles(d)
|
| 72 |
-
assert roles["task"] == "instruction" and roles["id"] == "task_id" and roles["answer"] == ["answer"]
|
| 73 |
-
assert roles["title"] == "question" # the instructions share a preamble: titled by the question
|
| 74 |
-
assert set(roles["grading"]) == {"reward_mode", "atol"} and roles["environment"] == ["files"]
|
| 75 |
-
view = p.view(rows[3], 3, d, roles)
|
| 76 |
-
assert '"3"' not in json.dumps([s["body"] for s in view["sections"] if s["id"] != "task"]) and view["withheld"] == ["answer"]
|
| 77 |
-
card = p.card(rows[3], 3, roles)
|
| 78 |
-
assert card["title"] == "what is 3?" and card["id"] == "t3"
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def test_nested_answers_are_withheld_everywhere():
|
| 82 |
-
clean, gone = base.withhold({"meta": {"oracle_commands": ["rm"], "level": 1}, "items": [{"gold": 1, "x": 2}], "prompt": "p"})
|
| 83 |
-
assert clean == {"meta": {"level": 1}, "items": [{"x": 2}], "prompt": "p"}
|
| 84 |
-
assert set(gone) == {"meta.oracle_commands", "items.gold"}
|
| 85 |
-
clean, _ = base.withhold({"label": "tw_1", "x": 1}, keep=("label",)) # a label that is the row's id stays
|
| 86 |
-
assert clean["label"] == "tw_1"
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
def test_retitle_drops_a_shared_preamble():
|
| 90 |
-
cards = [{"title": "Setup", "snippet": "", "_text": f"# Setup\nrun setup.sh\n\n# Goal\nTask number {i} is hard\nmore"} for i in range(4)]
|
| 91 |
-
base.retitle(cards)
|
| 92 |
-
assert [c["title"] for c in cards] == [f"Task number {i} is hard" for i in range(4)]
|
| 93 |
-
assert all("_text" not in c for c in cards)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
# ── Harbor tasks packed into rows ────────────────────────────────────────────
|
| 97 |
-
def test_packed_tasks_unpack_without_escaping_and_withhold_answers():
|
| 98 |
-
files = processors.unpack(tarball(TASK))
|
| 99 |
-
assert "task.toml" in files and "tests/test.sh" in files # the folder around them stripped
|
| 100 |
-
assert not any(".." in n for n in files)
|
| 101 |
-
p, d = processors.PackedHarbor(), ds(["path", "task_binary"], [], tags=["library:harbor"])
|
| 102 |
-
row = {"path": "task_0", "task_binary": tarball(TASK)}
|
| 103 |
-
view = p.view(row, 0, d, p.roles(d))
|
| 104 |
-
text = json.dumps(view)
|
| 105 |
-
assert "gold_answer" not in json.dumps([s for s in view["sections"] if s["id"] == "metadata"])
|
| 106 |
-
assert "solution/solve.sh" in view["withheld"] and "tests/expected_output.json" in view["withheld"] and "task_manifest.json" in view["withheld"]
|
| 107 |
-
assert p.file(row, d, "task_manifest.json")["withheld"] is True
|
| 108 |
-
assert view["title"] == "Count the words" and "Count the words in /app/a.txt" in text
|
| 109 |
-
assert p.file(row, d, "solution/solve.sh") == {"path": "solution/solve.sh", "size": 8, "withheld": True}
|
| 110 |
-
assert p.file(row, d, "tests/expected_output.json")["withheld"] is True
|
| 111 |
-
assert "42" not in p.file(row, d, "task.toml")["text"]
|
| 112 |
-
assert "42" not in p.file(row, d, "tests/test.sh")["text"] # expected=42 masked
|
| 113 |
-
assert p.file(row, d, "../escape.txt")["error"]
|
| 114 |
-
card = p.card(row, 0, {})
|
| 115 |
-
assert card["title"] == "Count the words" and "easy" in card["chips"]
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
def test_data_files_naming_the_answer_are_withheld():
|
| 119 |
-
assert catalog._withheld("task_manifest.json", json.dumps({"original_nl": "do x", "mutated_bash": "grep -c x f", "captured_output_path": "tests/o"}))
|
| 120 |
-
assert catalog._withheld("meta.yaml", "task: x\nexpected_output: 42\n")
|
| 121 |
-
assert catalog._withheld("data.jsonl", '{"q": 1, "answer": 2}\n')
|
| 122 |
-
assert not catalog._withheld("config.json", json.dumps({"captured_output_path": "x", "timeout": 3, "name": "t"}))
|
| 123 |
-
assert not catalog._withheld("instruction.md", "the expected_output is printed") # only data files by key
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
def test_bad_archives_are_empty_not_errors():
|
| 127 |
-
assert processors.unpack("not base64 !!!") == {}
|
| 128 |
-
assert processors.unpack(base64.b64encode(b"\x00" * 64).decode()) == {}
|
| 129 |
-
assert processors.unpack(12) == {}
|
| 130 |
-
big = base64.b64encode(b"x" * (processors.MAX_TAR + 10)).decode()
|
| 131 |
-
assert processors.unpack(big) == {}
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
# ── MiMo's raw release ───────────────────────────────────────────────────────
|
| 135 |
-
def test_mimo_rows_withhold_patches_and_link_their_harbor_twin(monkeypatch):
|
| 136 |
-
inst = {"instance_id": "Repo_Task-001", "docker_image": "img:1", "problem_statement": "fix it", "test_patch": "diff --git a/t b/t",
|
| 137 |
-
"patch": "diff --git a/src b/src SECRET", "poc": "crash bytes"}
|
| 138 |
-
row = {"data_source": "x", "ability": "swe", "agent_name": "a", "prompt": [{"role": "user", "content": "fix it"}],
|
| 139 |
-
"reward_model": {"style": "rule", "ground_truth": "SECRET2"}, "extra_info": {"instance_id": "Repo_Task-001", "instance_json": json.dumps(inst)}}
|
| 140 |
-
monkeypatch.setattr(catalog, "_read_index", lambda spec, sha=None: {"tasks": [{"path": "tasks/repo_task-001"}]} if spec.endswith("-code") else None)
|
| 141 |
-
p = processors.MiMo()
|
| 142 |
-
d = ds(list(row), [row], spec="XiaomiMiMo/MiMo-V2.6-RL-oss")
|
| 143 |
-
roles = {**p.roles(d), "_config": "code"}
|
| 144 |
-
view = p.view(row, 0, d, roles)
|
| 145 |
-
text = json.dumps(view)
|
| 146 |
-
assert "SECRET" not in text and "crash bytes" not in text
|
| 147 |
-
assert view["run"]["dataset"] == "FineEnvs/MiMo-V2.6-RL-harbor-code" and view["run"]["path"] == "tasks/repo_task-001"
|
| 148 |
-
assert any(s["id"] == "tests" for s in view["sections"]) # hidden tests are shown: they're how it's graded
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
# ── filters and the direct reader ────────────────────────────────────────────
|
| 152 |
-
def test_filters_are_quoted_and_limited_to_known_columns():
|
| 153 |
-
w = envs._where({"difficulty": "x' OR '1'='1", "nope": "1", 'a"b': "v"}, ["difficulty", 'a"b'])
|
| 154 |
-
assert w == "\"difficulty\"='x'' OR ''1''=''1' AND \"a\"\"b\"='v'"
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
def test_env_api_rejects_bad_filter_json():
|
| 158 |
-
assert client.get("/api/env/org/ds/tasks?f=nope").status_code == 400
|
| 159 |
-
assert client.get("/api/env/org/ds/tasks?f=[1]").status_code == 400
|
| 160 |
-
assert client.get("/api/env/org/ds/tasks?f=" + json.dumps({f"k{i}": ["v"] for i in range(20)})).status_code == 400
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
def test_jsonl_rows_page_by_line(tmp_path, monkeypatch):
|
| 164 |
-
p = tmp_path / "data.jsonl"
|
| 165 |
-
p.write_text("\n".join(json.dumps({"id": i, "prompt": f"p{i}"}) for i in range(10)) + "\n\n")
|
| 166 |
-
monkeypatch.setattr(direct, "_background", lambda *a, **k: {"state": "done", "local": str(p)})
|
| 167 |
-
monkeypatch.setattr(direct.Source, "_size", lambda self, path: 100)
|
| 168 |
-
src = direct.Source("org/ds", {"sha": "abc"}, ["data.jsonl"], None)
|
| 169 |
-
assert src.total() == 10
|
| 170 |
-
assert [i for i, _ in src.read(8, 5)] == [8, 9] and src.read(8, 5)[0][1]["prompt"] == "p8"
|
| 171 |
-
hits, cut = direct.scan(src, lambda r: r["id"] % 3 == 0)
|
| 172 |
-
assert [i for i, _ in hits] == [0, 3, 6, 9] and not cut
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
def test_a_file_still_coming_serves_its_first_rows(monkeypatch):
|
| 176 |
-
monkeypatch.setattr(direct, "_background", lambda *a, **k: {"state": "running"})
|
| 177 |
-
monkeypatch.setattr(direct, "_head", lambda *a, **k: ([{"i": n} for n in range(30)], False))
|
| 178 |
-
monkeypatch.setattr(direct.Source, "_size", lambda self, path: 10**9)
|
| 179 |
-
src = direct.Source("org/ds", {"sha": "abc"}, ["big.jsonl"], None)
|
| 180 |
-
assert src.total() is None
|
| 181 |
-
assert [r["i"] for _, r in src.read(0, 24)] == list(range(24))
|
| 182 |
-
with pytest.raises(direct.DirectError) as e:
|
| 183 |
-
src.read(100, 24)
|
| 184 |
-
assert e.value.status == 409
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def test_data_files_follow_the_card_configs(monkeypatch):
|
| 188 |
-
files = [("data/train-00000.parquet", 10), ("data/test-00000.parquet", 10), ("extra/notes.json", 5)]
|
| 189 |
-
monkeypatch.setattr(catalog, "_cached", lambda key, ttl, fn: files if key[0] == "data-files" else {
|
| 190 |
-
"configs": [{"config_name": "default", "data_files": [{"split": "train", "path": "data/train-*"}, {"split": "test", "path": "data/test-*"}]}]})
|
| 191 |
-
got = direct.data_files("org/ds", {"sha": "abc"}, None)
|
| 192 |
-
assert got == {"default": {"train": ["data/train-00000.parquet"], "test": ["data/test-00000.parquet"]}}
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
# ── big Harbor datasets, walked folder by folder ─────────────────────────────
|
| 196 |
-
class FakeHub:
|
| 197 |
-
"""A repo of 3 splits x 40 task folders, each with a big app folder that must never be listed."""
|
| 198 |
-
|
| 199 |
-
def __init__(self):
|
| 200 |
-
self.listed, self.info_calls = [], 0
|
| 201 |
-
self.dirs = {"": ["tasks"], "tasks": ["tasks/a", "tasks/b", "tasks/c"]}
|
| 202 |
-
self.files = {}
|
| 203 |
-
for s in ("a", "b", "c"):
|
| 204 |
-
self.dirs[f"tasks/{s}"] = [f"tasks/{s}/t{i}" for i in range(40)]
|
| 205 |
-
for i in range(40):
|
| 206 |
-
d = f"tasks/{s}/t{i}"
|
| 207 |
-
self.files.update({f"{d}/task.toml": 10, f"{d}/instruction.md": 20, f"{d}/tests/test.sh": 5})
|
| 208 |
-
self.dirs[d] = [f"{d}/app", f"{d}/tests"]
|
| 209 |
-
|
| 210 |
-
def list_repo_tree(self, spec, repo_type=None, revision=None, path_in_repo=None, recursive=False):
|
| 211 |
-
from huggingface_hub.hf_api import RepoFile, RepoFolder
|
| 212 |
-
|
| 213 |
-
p = path_in_repo or ""
|
| 214 |
-
self.listed.append(p)
|
| 215 |
-
assert not p.endswith("/app"), "a task's app folder was listed"
|
| 216 |
-
out = [RepoFolder(path=d, oid="x", tree_id="x") for d in self.dirs.get(p, [])]
|
| 217 |
-
out += [RepoFile(path=f, size=n, oid="x") for f, n in self.files.items() if f.rsplit("/", 1)[0] == p]
|
| 218 |
-
return iter(out)
|
| 219 |
-
|
| 220 |
-
def get_paths_info(self, spec, paths, repo_type=None, revision=None):
|
| 221 |
-
from huggingface_hub.hf_api import RepoFile
|
| 222 |
-
|
| 223 |
-
self.info_calls += 1
|
| 224 |
-
assert len(paths) <= 500
|
| 225 |
-
return [RepoFile(path=p, size=self.files[p], oid="x") for p in paths if p in self.files]
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
def test_walker_finds_every_task_with_few_calls(monkeypatch):
|
| 229 |
-
hub = FakeHub()
|
| 230 |
-
monkeypatch.setattr(catalog, "_api", lambda token=None: hub)
|
| 231 |
-
job = {}
|
| 232 |
-
out = catalog._walk("org/big", "abc", None, job)
|
| 233 |
-
assert len(out["tasks"]) == 120 and out["tasks"][0] == "tasks/a/t0"
|
| 234 |
-
assert out["files"]["tasks/a/t0"] == {"task.toml": 10, "instruction.md": 20, "tests/test.sh": 5}
|
| 235 |
-
assert len(hub.listed) <= 6 # root, tasks/, three splits (+0): never a task folder
|
| 236 |
-
assert hub.info_calls <= 6 and job["tasks"] == 120
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
def test_a_task_folder_listing_stays_inside_the_task(monkeypatch):
|
| 240 |
-
monkeypatch.setattr(catalog, "_task_row", lambda spec, path, token=None: ({"path": path}, {"sha": "abc", "info": {}}))
|
| 241 |
-
for bad in ("../other", "..", "", "/", "a/../../b"):
|
| 242 |
-
with pytest.raises(ValueError):
|
| 243 |
-
catalog.task_folder("org/ds", "tasks/t1", bad)
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
def test_listing_falls_back_to_walking_when_too_slow(monkeypatch):
|
| 247 |
-
hub = FakeHub()
|
| 248 |
-
|
| 249 |
-
def slow(*a, recursive=False, **k):
|
| 250 |
-
if recursive:
|
| 251 |
-
for n in range(10_000):
|
| 252 |
-
yield SimpleNamespace(path=f"f{n}")
|
| 253 |
-
else:
|
| 254 |
-
yield from hub.list_repo_tree(*a, recursive=False, **k)
|
| 255 |
-
|
| 256 |
-
monkeypatch.setattr(catalog, "_api", lambda token=None: SimpleNamespace(list_repo_tree=slow, get_paths_info=hub.get_paths_info))
|
| 257 |
-
monkeypatch.setattr(catalog, "LIST_BUDGET", -1)
|
| 258 |
-
files, walked = catalog._listing("org/big", "abc", None, {})
|
| 259 |
-
assert files == {} and walked and len(walked["tasks"]) == 120
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
# ── when the viewer can't filter or search ───────────────────────────────────
|
| 263 |
-
def test_filter_and_search_fall_back_to_reading_a_small_split(monkeypatch):
|
| 264 |
-
rows = [(i, {"id": f"t{i}", "q": f"question {i}", "tier": "hard" if i % 3 == 0 else "easy", "answer": "zebra" if i == 4 else "x"}) for i in range(50)]
|
| 265 |
-
|
| 266 |
-
def no(*a, **k):
|
| 267 |
-
raise envs.ViewerError("the dataset index is loading")
|
| 268 |
-
|
| 269 |
-
monkeypatch.setattr(envs.viewer, "filter_rows", no)
|
| 270 |
-
monkeypatch.setattr(envs.viewer, "search", no)
|
| 271 |
-
monkeypatch.setattr(envs.rows, "_all_rows", lambda spec, config, split, tok: rows)
|
| 272 |
-
envs._broken.clear()
|
| 273 |
-
v = envs.Viewer("org/ds", None)
|
| 274 |
-
page, _ = v.filter("default", "train", {"tier": "hard"}, ["tier"], 0)
|
| 275 |
-
assert page["total"] == 17 and all(r["tier"] == "hard" for _, r, _ in page["rows"])
|
| 276 |
-
page, _ = v.search("default", "train", "question 4", 0)
|
| 277 |
-
assert {r["id"] for _, r, _ in page["rows"]} >= {"t4", "t40", "t41"}
|
| 278 |
-
page, _ = v.search("default", "train", "zebra", 0)
|
| 279 |
-
assert page["total"] == 0 # searching an answer finds nothing
|
| 280 |
-
assert ("org/ds", "filter") in envs._broken # next time straight to the fallback
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
def test_one_slow_viewer_answer_does_not_send_a_dataset_to_its_files(monkeypatch):
|
| 284 |
-
calls = {"n": 0}
|
| 285 |
-
|
| 286 |
-
def flaky(spec, tok=None):
|
| 287 |
-
calls["n"] += 1
|
| 288 |
-
if calls["n"] == 1:
|
| 289 |
-
raise envs.ViewerError("timeout", 502)
|
| 290 |
-
return [{"config": "default", "split": "train"}]
|
| 291 |
-
|
| 292 |
-
monkeypatch.setattr(catalog, "info", lambda spec, token=None: {"sha": "abc", "restricted": False})
|
| 293 |
-
monkeypatch.setattr(envs.viewer, "splits", flaky)
|
| 294 |
-
monkeypatch.setattr(envs.viewer, "rows", lambda *a, **k: {"rows": [], "total": 0, "features": []})
|
| 295 |
-
envs._choice.clear()
|
| 296 |
-
_, be = envs._backend("org/ds", None)
|
| 297 |
-
assert be.kind == "viewer" and calls["n"] == 2
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
def test_harbor_layouts_are_found_whatever_the_tags(monkeypatch):
|
| 301 |
-
hub = FakeHub()
|
| 302 |
-
monkeypatch.setattr(catalog, "_api", lambda token=None: hub)
|
| 303 |
-
catalog._memo.clear()
|
| 304 |
-
assert catalog.looks_harbor("org/big", "abc") is True
|
| 305 |
-
hub.files = {}
|
| 306 |
-
catalog._memo.clear()
|
| 307 |
-
assert catalog.looks_harbor("org/big", "abc") is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_frameworks.py
DELETED
|
@@ -1,420 +0,0 @@
|
|
| 1 |
-
"""The framework readers: NeMo Gym (app/envs/nemogym.py), verl / SkyRL (verl_rows.py) and Verifiers (verifiers_rows.py),
|
| 2 |
-
with the conversation and tool reader they share (convo.py). No network: rows are built here in each framework's real
|
| 3 |
-
field names, and the Prime Environments Hub and file listings are stubbed.
|
| 4 |
-
|
| 5 |
-
Checked first and most: no answer ever leaves a reader (not in the task view, its cards, the dataset overview, nor
|
| 6 |
-
what search looks at), for every framework's real answer fields. Then: each reader says what the policy is asked
|
| 7 |
-
(the conversation, its tools), how it's graded and by what, and how to run it with its own framework, honestly not
|
| 8 |
-
here.
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
from __future__ import annotations
|
| 12 |
-
|
| 13 |
-
import json
|
| 14 |
-
|
| 15 |
-
import pytest
|
| 16 |
-
|
| 17 |
-
from app.envs import base, convo, nemogym, processors, rows, verifiers_rows, verl_rows
|
| 18 |
-
from app.envs.base import withhold
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
def ds(columns, sample, spec="org/ds", tags=(), splits=None):
|
| 22 |
-
return base.Dataset(spec=spec, tags=list(tags), card={}, splits=splits or [{"config": "default", "split": "train"}],
|
| 23 |
-
features=[{"name": c, "type": {}} for c in columns], sample=sample)
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
def everything(proc, row, spec="org/ds", tags=(), splits=None):
|
| 27 |
-
"""A row through a reader the way the pages get it: its view, its card, and the dataset's overview."""
|
| 28 |
-
d = ds(list(row), [row], spec=spec, tags=tags, splits=splits)
|
| 29 |
-
roles = {**proc.roles(d), "_config": "default", "_split": "train"}
|
| 30 |
-
view = proc.view(row, 0, d, roles)
|
| 31 |
-
card = proc.card(row, 0, roles)
|
| 32 |
-
overview = proc.overview(d, roles, [], "default", "train")
|
| 33 |
-
return view, card, overview, roles, json.dumps([view, card, overview, {k: v for k, v in roles.items() if not k.startswith("_")}], ensure_ascii=False)
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
@pytest.fixture(autouse=True)
|
| 37 |
-
def _offline(monkeypatch):
|
| 38 |
-
monkeypatch.setattr(verifiers_rows, "_hub", lambda env_id: {"runtime": "VERIFIERS_V1", "description": "a taskset"})
|
| 39 |
-
monkeypatch.setattr(verifiers_rows, "_card_env", lambda spec, meta: None)
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
# ── conversations and tools ──────────────────────────────────────────────────
|
| 43 |
-
RESPONSES_INPUT = [
|
| 44 |
-
{"role": "system", "content": "You are a finance assistant."},
|
| 45 |
-
{"role": "user", "content": "Pull up the balance sheet for Suncor."},
|
| 46 |
-
{"id": "rs_1", "type": "reasoning", "summary": [{"type": "summary_text", "text": "I need the symbol."}]},
|
| 47 |
-
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Is **SU** right?", "annotations": []}]},
|
| 48 |
-
{"role": "user", "content": "Yes, SU."},
|
| 49 |
-
{"type": "function_call", "name": "get_balance_sheet", "arguments": "{\"symbol\": \"SU\"}", "call_id": "c1"},
|
| 50 |
-
{"type": "function_call_output", "call_id": "c1", "output": "{\"assets\": 1}"},
|
| 51 |
-
{"role": "user", "content": "Now the earnings, please."},
|
| 52 |
-
]
|
| 53 |
-
TOOLS = [{"type": "function", "name": "get_balance_sheet", "description": "Balance sheets. Annual and quarterly.", "strict": True,
|
| 54 |
-
"parameters": {"type": "object", "properties": {"symbol": {"type": "string", "description": "Ticker"},
|
| 55 |
-
"period": {"type": "string", "enum": ["annual", "quarter"]}}, "required": ["symbol"]}},
|
| 56 |
-
{"type": "function", "function": {"name": "get_earnings", "description": "EPS", "parameters": {"type": "object", "properties": {
|
| 57 |
-
"symbol": {"type": "string"}, "years": {"type": "array", "items": {"type": "integer"}}}}}}]
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def test_responses_items_become_a_transcript_with_outputs_paired_to_calls():
|
| 61 |
-
ts = convo.turns(RESPONSES_INPUT)
|
| 62 |
-
kinds = [(t["kind"], t["role"]) for t in ts]
|
| 63 |
-
assert kinds == [("message", "system"), ("message", "user"), ("reasoning", "assistant"), ("message", "assistant"), ("message", "user"),
|
| 64 |
-
("call", "assistant"), ("message", "user")]
|
| 65 |
-
call = ts[5]
|
| 66 |
-
assert call["name"] == "get_balance_sheet" and json.loads(call["args"]) == {"symbol": "SU"} and "\n" in call["args"] # pretty-printed
|
| 67 |
-
assert json.loads(call["output"]) == {"assets": 1} # JSON outputs are pretty-printed too
|
| 68 |
-
assert ts[3]["text"] == "Is **SU** right?" and ts[2]["text"] == "I need the symbol."
|
| 69 |
-
assert convo.first_user(ts).startswith("Pull up") and convo.last_user(ts) == "Now the earnings, please."
|
| 70 |
-
assert "get_balance_sheet" in convo.transcript_text(ts)
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
def test_chat_tool_calls_pair_with_tool_replies_in_order():
|
| 74 |
-
msgs = [{"role": "user", "content": "go"},
|
| 75 |
-
{"role": "assistant", "content": None, "reasoning_content": "think", "tool_calls": [
|
| 76 |
-
{"id": "a", "type": "function", "function": {"name": "f", "arguments": "{}"}},
|
| 77 |
-
{"id": "b", "type": "function", "function": {"name": "g", "arguments": "{\"x\": 1}"}}]},
|
| 78 |
-
{"role": "tool", "tool_call_id": "b", "content": "from g"}, {"role": "tool", "content": "from f"}]
|
| 79 |
-
ts = convo.turns(msgs)
|
| 80 |
-
calls = [t for t in ts if t["kind"] == "call"]
|
| 81 |
-
assert [c["output"] for c in calls] == ["from f", "from g"]
|
| 82 |
-
assert ts[1]["kind"] == "reasoning" and not any(t["kind"] == "output" for t in ts)
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
def test_tools_read_as_signatures_whatever_their_shape():
|
| 86 |
-
ts = convo.tools(TOOLS)
|
| 87 |
-
assert [t["name"] for t in ts] == ["get_balance_sheet", "get_earnings"]
|
| 88 |
-
sym, period = ts[0]["params"]
|
| 89 |
-
assert sym == {"name": "symbol", "type": "string", "required": True, "description": "Ticker"}
|
| 90 |
-
assert period["enum"] == ["annual", "quarter"] and not period["required"]
|
| 91 |
-
assert ts[1]["params"][1]["type"] == "array of integer"
|
| 92 |
-
assert convo.tools([{"name": "mcp_tool", "input_schema": {"properties": {"q": {"type": "string"}}, "required": ["q"]}}])[0]["params"][0]["required"]
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def test_titles_skip_tags_templates_and_preambles():
|
| 96 |
-
assert convo.task_title("<uploaded_files>\n/workspace/x\n</uploaded_files>\n\n<issue>\n**Title:** Mask ignored on assignment\n") == "Mask ignored on assignment"
|
| 97 |
-
tpl = "Answer the following question. Reply with 'Answer: X'.\n\n{problem}"
|
| 98 |
-
assert convo.task_title("Answer the following question. Reply with 'Answer: A/B'.\n\nWhat is 2+2?\nA: 4\nB: 5", tpl) == "What is 2+2?"
|
| 99 |
-
assert convo.headline("Task:\nIn the sheet, sum column B.") == "In the sheet, sum column B."
|
| 100 |
-
assert convo.headline("<image> As shown in the figure, find x.") == "As shown in the figure, find x."
|
| 101 |
-
long = "We need a shortlist of films " + "with many constraints " * 12
|
| 102 |
-
assert convo.headline(long).endswith("…") and len(convo.headline(long)) <= 182
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
def test_withhold_takes_dotted_names_through_lists():
|
| 106 |
-
v, gone = withhold({"rubric": [{"question": "Q1?", "pass_criteria": "YES"}], "verifier_metadata": {"unit_tests": {"outputs": ["9"]}, "x": 1}},
|
| 107 |
-
hide=frozenset({"rubric.pass_criteria", "verifier_metadata.unit_tests"}))
|
| 108 |
-
assert v == {"rubric": [{"question": "Q1?"}], "verifier_metadata": {"x": 1}}
|
| 109 |
-
assert gone == ["rubric.pass_criteria", "verifier_metadata.unit_tests"]
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
# ── NeMo Gym ─────────────────────────────────────────────────────────────────
|
| 113 |
-
def pivot_row():
|
| 114 |
-
return {"trajectory_id": 3344, "info": {"turn": 6, "step": 1, "depth": 12},
|
| 115 |
-
"responses_create_params": {"input": RESPONSES_INPUT, "tools": TOOLS, "parallel_tool_calls": True},
|
| 116 |
-
"expected_action": {"type": "function_call", "name": "get_earnings", "arguments": "{\"symbol\": \"SECRET-NEXT-STEP\"}"},
|
| 117 |
-
"agent_ref": {"type": "responses_api_agents", "name": "toolcall_schema_single_step_tool_use_with_argument_comparison_agent"}}
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
def test_nemo_gym_pivot_row_reads_as_a_conversation_and_hides_the_experts_next_step():
|
| 121 |
-
view, card, overview, roles, text = everything(nemogym.NemoGym(), pivot_row(), spec="nvidia/Nemotron-RL-Agentic-Function-Calling-Pivot-v1")
|
| 122 |
-
assert "SECRET-NEXT-STEP" not in text
|
| 123 |
-
assert view["withheld"] == ["expected_action"] and roles["answer"] == ["expected_action"]
|
| 124 |
-
assert view["title"] == "Pull up the balance sheet for Suncor." and card["snippet"].startswith("Latest: Now the earnings")
|
| 125 |
-
sec = {s["id"]: s for s in view["sections"]}
|
| 126 |
-
tr = sec["task"]["body"][0]
|
| 127 |
-
assert tr["type"] == "custom" and tr["renderer"] == "rl" and tr["kind"] == "transcript"
|
| 128 |
-
assert any(t["kind"] == "call" and t.get("output") for t in tr["data"]["turns"]) and "expert" in tr["data"]["next"]["text"]
|
| 129 |
-
assert sec["tools"]["body"][0]["kind"] == "tools" and len(sec["tools"]["body"][0]["data"]["tools"]) == 2
|
| 130 |
-
grading = json.dumps(sec["grading"]["body"])
|
| 131 |
-
assert "single_step_tool_use_with_argument_comparison" in grading and "expected_action" in grading
|
| 132 |
-
run = sec["run"]["body"][0]["text"]
|
| 133 |
-
assert "gym env start" in run and "resources_servers/single_step_tool_use_with_argument_comparison/configs/" \
|
| 134 |
-
"toolcall_schema_single_step_tool_use_with_argument_comparison.yaml" in run
|
| 135 |
-
assert "--agent toolcall_schema_single_step_tool_use_with_argument_comparison_agent" in run and "gym eval run --no-serve" in run
|
| 136 |
-
assert "--repo-id nvidia/Nemotron-RL-Agentic-Function-Calling-Pivot-v1" in run and "sed -n '1p'" in run
|
| 137 |
-
opt = view["framework_run"]
|
| 138 |
-
# This browsing fixture mixes Chat Completions and Responses tool formats;
|
| 139 |
-
# execution requires the dataset's native Responses function definitions.
|
| 140 |
-
assert opt["runner"] == "nemo-gym" and opt["ok"] is False and "native NeMo Gym" in opt["why"]
|
| 141 |
-
assert ["Step", "turn 6 · step 1"] in view["glance"] and card["chips"][0] == "turn 6 · step 1"
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
NEMO_ANSWERS = [
|
| 145 |
-
# (row, the secret strings, the withheld names)
|
| 146 |
-
({"responses_create_params": {"input": [{"role": "user", "content": "Which? A: x B: y"}]}, "expected_answer": "SECRET-LETTER-C",
|
| 147 |
-
"options": [{"A": "x"}, {"B": "y"}], "agent_ref": {"type": "responses_api_agents", "name": "mcqa_simple_agent"}},
|
| 148 |
-
["SECRET-LETTER-C"], ["expected_answer"]),
|
| 149 |
-
({"id": 0, "responses_create_params": {"input": [{"role": "user", "content": "Reply to carlos"}], "tools": TOOLS},
|
| 150 |
-
"ground_truth": [{"name": "email_reply_email", "arguments": "{\"body\": \"SECRET-REPLY\"}"}], "category": "email",
|
| 151 |
-
"environment_name": "workplace_assistant", "agent_ref": {"type": "responses_api_agents", "name": "workplace_assistant_simple_agent"}},
|
| 152 |
-
["SECRET-REPLY"], ["ground_truth"]),
|
| 153 |
-
({"responses_create_params": {"input": [{"role": "user", "content": "add a meeting"}]}, "exp_cal_state": {"0": {"min_time": "SECRET-10:00"}},
|
| 154 |
-
"agent_ref": {"type": "responses_api_agents", "name": "calendar_simple_agent"}}, ["SECRET-10:00"], ["exp_cal_state"]),
|
| 155 |
-
({"responses_create_params": {"input": [{"role": "user", "content": "solve"}]}, "verifier_metadata": {"unit_tests": {"inputs": ["1"], "outputs": ["SECRET-OUT"]}},
|
| 156 |
-
"hash_id": "h", "agent_ref": {"type": "responses_api_agents", "name": "code_gen_simple_agent"}}, ["SECRET-OUT"], ["verifier_metadata.unit_tests"]),
|
| 157 |
-
({"trajectory_id": 1, "responses_create_params": {"input": [{"role": "user", "content": "<issue>fix</issue>"}]},
|
| 158 |
-
"ref_patch": "SECRET-PATCH", "ref_message": {"role": "assistant", "content": "SECRET-MSG"}, "expected_action": {"type": "message", "content": "SECRET-ACT"},
|
| 159 |
-
"agent_ref": {"type": "responses_api_agents", "name": "single_step_tool_use_with_argument_comparison_swe"}},
|
| 160 |
-
["SECRET-PATCH", "SECRET-MSG", "SECRET-ACT"], ["expected_action", "ref_message", "ref_patch"]),
|
| 161 |
-
({"uuid": "u", "responses_create_params": {"input": [{"role": "user", "content": "plan a snack"}]},
|
| 162 |
-
"rubric": [{"question": "Did it avoid peanuts?", "pass_criteria": "SECRET-YES"}], "agent_ref": {"type": "responses_api_agents", "name": "multichallenge_simple_agent"}},
|
| 163 |
-
["SECRET-YES"], ["rubric.pass_criteria"]),
|
| 164 |
-
({"task_id": "t", "responses_create_params": {"input": [{"role": "system", "content": "bank"}]}, "customer": "C1", "opening_message": "Hi, a statement please",
|
| 165 |
-
"user_scenario": {"persona": "p"}, "initial_state": {}, "evaluation_criteria": {"actions": [{"name": "SECRET-ACTION"}], "communicate_info": ["SECRET-FACT"],
|
| 166 |
-
"nl_assertions": ["no mutation"]}},
|
| 167 |
-
["SECRET-ACTION", "SECRET-FACT"], ["evaluation_criteria.actions", "evaluation_criteria.communicate_info"]),
|
| 168 |
-
({"id": "j", "responses_create_params": {"input": [{"role": "user", "content": "how to"}]}, "response_policy_mapped": "SECRET-POLICY",
|
| 169 |
-
"agent_ref": {"type": "responses_api_agents", "name": "jailbreak_detection_simple_agent"}}, ["SECRET-POLICY"], ["response_policy_mapped"]),
|
| 170 |
-
({"uuid": "r", "responses_create_params": {"input": [{"role": "user", "content": "knights"}]}, "question": "knights", "answer": "SECRET-ANS",
|
| 171 |
-
"metadata": {"solution": "SECRET-SOLUTION"}, "agent_ref": {"type": "responses_api_agents", "name": "reasoning_gym_simple_agent"}},
|
| 172 |
-
["SECRET-ANS", "SECRET-SOLUTION"], ["answer", "metadata"]),
|
| 173 |
-
({"responses_create_params": {"input": [{"role": "user", "content": "cite"}]}, "verifier": {"type": "string_match", "patterns": ["SECRET-RE"],
|
| 174 |
-
"expected_markers": ["SECRET-MARK"]},
|
| 175 |
-
"agent_ref": {"type": "responses_api_agents", "name": "citation_format_simple_agent"}}, ["SECRET-RE", "SECRET-MARK"], ["verifier"]),
|
| 176 |
-
]
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
@pytest.mark.parametrize(("row", "secrets", "names"), NEMO_ANSWERS)
|
| 180 |
-
def test_nemo_gym_answer_fields_never_leave(row, secrets, names):
|
| 181 |
-
view, card, overview, roles, text = everything(nemogym.NemoGym(), row)
|
| 182 |
-
for s in secrets:
|
| 183 |
-
assert s not in text, s
|
| 184 |
-
assert set(names) <= set(view["withheld"]), view["withheld"]
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def test_nemo_gym_names_the_config_that_runs_a_renamed_or_unnamed_agent():
|
| 188 |
-
a = nemogym.agent_of({"agent_ref": {"type": "responses_api_agents", "name": "turing_vif_simple_agent"}}, "org/x")
|
| 189 |
-
assert a["known"] and a["renamed"] and a["config_name"] == "verifif_simple_agent" and a["resources_server"] == "verifif"
|
| 190 |
-
a = nemogym.agent_of({"agent_ref": {"name": "single_step_tool_use_with_argument_comparison_swe"}}, "org/x")
|
| 191 |
-
assert a["config_name"] == "swe_pivot_single_step_tool_use_with_argument_comparison_agent"
|
| 192 |
-
a = nemogym.agent_of({"task_id": "t", "customer": "C", "user_scenario": {}, "evaluation_criteria": {}, "initial_state": {}, "opening_message": "hi"}, "org/x")
|
| 193 |
-
assert a["inferred"] and a["resources_server"] == "indian_banking"
|
| 194 |
-
a = nemogym.agent_of({"question": "q", "expected_answer": "1"}, "nvidia/Nemotron-RL-math-OpenMathReasoning") # no agent_ref: the config naming the dataset
|
| 195 |
-
assert a["resources_server"] == "math_with_judge"
|
| 196 |
-
a = nemogym.agent_of({"agent_ref": {"name": "nobody_knows_agent"}}, "org/x")
|
| 197 |
-
assert not a["known"] and a["name"] == "nobody_knows_agent"
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
def test_nemo_gym_blends_route_each_row_and_run_without_one_agent():
|
| 201 |
-
rows_ = [{"responses_create_params": {"input": [{"role": "user", "content": f"q{i}"}]},
|
| 202 |
-
"agent_ref": {"type": "responses_api_agents", "name": n}} for i, n in enumerate(["mcqa_simple_agent", "workplace_assistant_simple_agent"])]
|
| 203 |
-
d = ds(["responses_create_params", "agent_ref"], rows_, spec="nvidia/Nemotron-RL-Ultra-Training-Blends")
|
| 204 |
-
p = nemogym.NemoGym()
|
| 205 |
-
roles = p.roles(d)
|
| 206 |
-
assert roles["_mixed"] and p.card(rows_[0], 0, roles)["lead"] == "mcqa_simple_agent"
|
| 207 |
-
ov = p.overview(d, roles, [], "rlvr1", "train")
|
| 208 |
-
run = next(s for s in ov if s["id"] == "run")["blocks"][0]["text"]
|
| 209 |
-
assert run.count("--config ") == 2 and "--agent" not in run and "load_dataset('nvidia/Nemotron-RL-Ultra-Training-Blends', 'rlvr1'" in run
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
def test_nemo_gym_catalog_is_current_and_complete():
|
| 213 |
-
cat = nemogym.catalog()
|
| 214 |
-
assert len(cat["commit"]) == 40 and len(cat["agents"]) > 100 and len(cat["servers"]) > 100
|
| 215 |
-
assert cat["servers"]["instruction_following"]["verification"] == "Rule-based instruction checks"
|
| 216 |
-
assert cat["servers"]["math_proof_judgement"]["verification"] == "Deterministic answer parsing"
|
| 217 |
-
for name in ("mcqa_simple_agent", "workplace_assistant_simple_agent", "calendar_simple_agent", "code_gen_simple_agent",
|
| 218 |
-
"instruction_following_simple_agent", "reasoning_gym_simple_agent"):
|
| 219 |
-
a = cat["agents"][name]
|
| 220 |
-
assert a["config"].startswith("resources_servers/") and a["resources_server"] in cat["servers"]
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
# ── verl / SkyRL ─────────────────────────────────────────────────────────────
|
| 224 |
-
def verl_row(**kw):
|
| 225 |
-
return {"data_source": "openai/gsm8k", "prompt": [{"role": "user", "content": "Natalia sold clips to 48 friends. How many? Let's think step by step."}],
|
| 226 |
-
"ability": "math", "reward_model": {"style": "rule", "ground_truth": "SECRET-72"},
|
| 227 |
-
"extra_info": {"split": "train", "index": 0, "answer": "SECRET-WORKED-SOLUTION", "question": "Natalia sold clips to 48 friends. How many?",
|
| 228 |
-
"tools_kwargs": {"calc_gsm8k_reward": {"create_kwargs": {"ground_truth": "SECRET-TOOL-GT"}}}}, **kw}
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
def test_verl_rows_hide_every_ground_truth_and_name_their_scorer():
|
| 232 |
-
view, card, overview, roles, text = everything(verl_rows.Verl(), verl_row(agent_name="tool_agent"))
|
| 233 |
-
for s in ("SECRET-72", "SECRET-WORKED-SOLUTION", "SECRET-TOOL-GT"):
|
| 234 |
-
assert s not in text, s
|
| 235 |
-
assert {"reward_model.ground_truth", "extra_info.answer"} <= set(view["withheld"])
|
| 236 |
-
assert view["title"] == "Natalia sold clips to 48 friends. How many?" and ["Scorer", "`gsm8k`"] in view["glance"]
|
| 237 |
-
sec = {s["id"]: s for s in view["sections"]}
|
| 238 |
-
assert "calc_gsm8k_reward" in json.dumps(sec["tools"]["body"])
|
| 239 |
-
run = sec["run"]["body"][0]["text"]
|
| 240 |
-
assert "python3 -m verl.trainer.main_ppo" in run and "multi_turn.enable=True" in run and "custom_reward_function" not in run
|
| 241 |
-
assert view["framework_run"]["runner"] == "verl" and view["framework_run"]["ok"] is False
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
@pytest.mark.parametrize(("source", "scorer"), [("openai/gsm8k", "gsm8k"), ("HuggingFaceH4/MATH-500", "math_reward"), ("aime24", "math_dapo"),
|
| 245 |
-
("numina_olympiads", "prime_math"), ("codeforces", "sandbox_fusion / prime_code"),
|
| 246 |
-
("searchR1_nq", "search_r1_like_qa_em"), ("DeepScaleR-Preview", None)])
|
| 247 |
-
def test_verl_routes_data_sources_as_default_compute_score_does(source, scorer):
|
| 248 |
-
got = verl_rows.scorer(source)
|
| 249 |
-
assert (got[0] if got else None) == scorer
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
def test_verl_rows_with_no_built_in_scorer_get_a_custom_reward_in_the_command():
|
| 253 |
-
view, *_ = everything(verl_rows.Verl(), {**verl_row(), "data_source": "sheet_arena"})
|
| 254 |
-
run = next(s for s in view["sections"] if s["id"] == "run")["body"][0]["text"]
|
| 255 |
-
assert "reward.custom_reward_function.path=" in run
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
def test_compressed_blobs_and_skyrl_sql_answers_are_withheld():
|
| 259 |
-
blob = "H4sI" + "A" * 400
|
| 260 |
-
row = {"data_source": "synsql", "prompt": [{"role": "user", "content": "Write SQL for the question"}], "env_class": "text2sql",
|
| 261 |
-
"reward_spec": {"method": "rule", "ground_truth": "SECRET-GT"}, "sql": "SELECT SECRET_SQL", "output_seq": "SECRET-REASONING",
|
| 262 |
-
"db_id": "shop", "extra_info": {"part_1": blob}}
|
| 263 |
-
view, card, overview, roles, text = everything(verl_rows.Verl(), row)
|
| 264 |
-
for s in ("SECRET-GT", "SECRET_SQL", "SECRET-REASONING", blob):
|
| 265 |
-
assert s not in text
|
| 266 |
-
assert view["framework"] == "SkyRL" and "extra_info.part_1" in view["withheld"]
|
| 267 |
-
run = next(s for s in view["sections"] if s["id"] == "run")["body"][0]["text"]
|
| 268 |
-
assert "skyrl.train.entrypoints.main_base" in run and "environment.env_class=text2sql" in run and "org/ds:train" in run
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
def test_mimo_no_longer_claims_verl_rows_that_name_an_agent_loop():
|
| 272 |
-
row = {"data_source": "sheet_arena", "agent_name": "tool_agent", "prompt": [{"role": "user", "content": "x"}], "ability": "spreadsheet",
|
| 273 |
-
"reward_model": {"style": "rule", "ground_truth": "id"}, "extra_info": {"id": "x"}}
|
| 274 |
-
assert rows.pick(ds(list(row), [row])).id == "verl"
|
| 275 |
-
mimo = {**row, "extra_info": {"instance_json": "{}"}}
|
| 276 |
-
assert rows.pick(ds(list(mimo), [mimo])).id == "mimo"
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
# ── Verifiers ────────────────────────────────────────────────────────────────
|
| 280 |
-
def test_verifiers_swe_rows_withhold_gold_patches_tests_and_solutions():
|
| 281 |
-
row = {"repo_name": "orange3", "docker_image": "namanjain12/orange3:abc", "commit_hash": "abc",
|
| 282 |
-
"problem_statement": "[ISSUE]\n**Title:** Context migration fails\n\n**Description:** It raises.\n[/ISSUE]",
|
| 283 |
-
"prompt": "Write an issue for this commit SECRET-COMMIT-PROMPT", "parsed_commit_content": "SECRET-GOLD-DIFF",
|
| 284 |
-
"execution_result_content": "SECRET-TESTS", "expected_output_json": "{\"test_a\": \"SECRET-PASSED\"}", "modified_files": ["SECRET/file.py"],
|
| 285 |
-
"relevant_files": ["SECRET/rel.py"], "modified_entity_summaries": "SECRET-SUMMARY"}
|
| 286 |
-
view, card, overview, roles, text = everything(verifiers_rows.Verifiers(), row, spec="PrimeIntellect/R2E-Gym-Subset-Verified")
|
| 287 |
-
for s in ("SECRET-COMMIT-PROMPT", "SECRET-GOLD-DIFF", "SECRET-TESTS", "SECRET-PASSED", "SECRET/file.py", "SECRET/rel.py", "SECRET-SUMMARY"):
|
| 288 |
-
assert s not in text, s
|
| 289 |
-
assert view["title"] == "Context migration fails"
|
| 290 |
-
assert ["Image", "`namanjain12/orange3:abc`"] in view["glance"] and ["API", "v1"] in view["glance"]
|
| 291 |
-
run = next(s for s in view["sections"] if s["id"] == "run")["body"][0]["text"]
|
| 292 |
-
assert "prime env install primeintellect/r2e-gym" in run and "uv run eval primeintellect/r2e-gym" in run and "eval.toml" not in run
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
def test_multi_swe_bench_shows_the_issues_and_hides_the_fixing_pr():
|
| 296 |
-
row = {"org": "facebook", "repo": "zstd", "number": 3942, "title": "Fix #3719: SECRET-PR-TITLE", "body": "SECRET-PR-BODY",
|
| 297 |
-
"resolved_issues": {"title": ["zstd won't remove the file"], "body": ["Steps to reproduce"], "number": [3719]},
|
| 298 |
-
"fix_patch": "SECRET-FIX", "test_patch": "SECRET-TEST-PATCH", "f2p_tests": {"t": "SECRET-F2P"}, "run_result": {"x": "SECRET-RUN"},
|
| 299 |
-
"instance_id": "facebook__zstd-3942", "hints": "SECRET-HINT"}
|
| 300 |
-
view, card, overview, roles, text = everything(verifiers_rows.Verifiers(), row, spec="PrimeIntellect/Multi-SWE-bench",
|
| 301 |
-
splits=[{"config": "default", "split": "test"}])
|
| 302 |
-
for s in ("SECRET-PR-TITLE", "SECRET-PR-BODY", "SECRET-FIX", "SECRET-TEST-PATCH", "SECRET-F2P", "SECRET-RUN", "SECRET-HINT"):
|
| 303 |
-
assert s not in text, s
|
| 304 |
-
assert view["title"] == "zstd won't remove the file" and roles["task"] == "resolved_issues"
|
| 305 |
-
run = next(s for s in view["sections"] if s["id"] == "run")["body"][0]["text"]
|
| 306 |
-
assert 'dataset_name = "PrimeIntellect/Multi-SWE-bench"' in run and 'id = "primeintellect/multiswe"' in run # not the taskset's default dataset
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
def test_verifiers_v0_rows_show_the_prompt_and_withhold_the_answer():
|
| 310 |
-
row = {"question": "What is 6*7?", "answer": "SECRET-42", "info": {"difficulty": "easy", "solution": "SECRET-STEPS"}, "task": "arith"}
|
| 311 |
-
view, card, overview, roles, text = everything(verifiers_rows.Verifiers(), row, tags=["verifiers"])
|
| 312 |
-
assert "SECRET-42" not in text and "SECRET-STEPS" not in text
|
| 313 |
-
assert view["title"] == "What is 6*7?" and {"answer", "info.solution"} <= set(view["withheld"])
|
| 314 |
-
assert "difficulty" in json.dumps(view["sections"])
|
| 315 |
-
run = next(s for s in view["sections"] if s["id"] == "run")["body"][0]["text"]
|
| 316 |
-
assert "prime env install <owner>/<environment>" in run # no environment named: says how to find it, invents none
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
def test_verifiers_known_v0_environment_uses_the_legacy_runner(monkeypatch):
|
| 320 |
-
monkeypatch.setattr(verifiers_rows, "_hub", lambda env_id: {"runtime": "VERIFIERS_V0"})
|
| 321 |
-
view, *_ = everything(verifiers_rows.Verifiers(), {"prompt": "The community in Bruck was merged into it"}, spec="PrimeIntellect/Reverse-Text-RL")
|
| 322 |
-
task = next(s for s in view["sections"] if s["id"] == "task")["body"][0]["data"]["turns"]
|
| 323 |
-
assert task[0]["role"] == "system" and "<reversed_text>" in task[0]["text"]
|
| 324 |
-
run = next(s for s in view["sections"] if s["id"] == "run")["body"][0]["text"]
|
| 325 |
-
assert "uv run vf-eval reverse-text" in run and "prime env install primeintellect/reverse-text" in run
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
def test_rule_induction_rows_hide_the_rule():
|
| 329 |
-
row = {"rule_id": "red_cards", "label": "red cards", "family": "single_axis", "code": "return card.color == 'red'"}
|
| 330 |
-
view, card, overview, roles, text = everything(verifiers_rows.Verifiers(), row, spec="nph4rd/eleusis-simple-rules", tags=["verifiers"])
|
| 331 |
-
assert "red_cards" not in text and "red cards" not in text and "card.color" not in text
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
# ── the rows adapter ─────────────────────────────────────────────────────────
|
| 335 |
-
def test_search_never_looks_at_framework_answers():
|
| 336 |
-
row = {"expected_action": {"content": "SECRET"}, "ref_patch": "SECRET", "exp_cal_state": "SECRET", "fix_patch": "SECRET", "prompt": "find me"}
|
| 337 |
-
assert "SECRET" not in json.dumps(rows._searchable(row)) and "find me" in json.dumps(rows._searchable(row))
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
def test_framework_rows_offer_their_own_framework_as_the_way_to_run():
|
| 341 |
-
opt = nemogym.run_option({"resources_server": "mcqa"})
|
| 342 |
-
assert rows.RowsAdapter().run_options(None, "train/0", {"_framework_run": opt}) == [opt]
|
| 343 |
-
assert opt["ok"] is False and opt["why"] and "**Run it with NeMo Gym**" in opt["about"]
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
def test_a_dataset_opens_on_its_train_split_not_its_leftovers():
|
| 347 |
-
assert rows._first([{"config": "default", "split": "dropped"}, {"config": "default", "split": "resolved"}])["split"] == "resolved"
|
| 348 |
-
assert rows._first([{"config": "a", "split": "test"}, {"config": "a", "split": "train"}, {"config": "b", "split": "train"}]) == {"config": "a", "split": "train"}
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
def test_processors_hidden_union_covers_every_framework():
|
| 352 |
-
assert {"expected_action", "sql", "fix_patch"} <= processors.HIDDEN
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
def test_a_config_the_viewer_cant_read_is_read_from_its_files(monkeypatch):
|
| 356 |
-
class BrokenViewer:
|
| 357 |
-
kind, tok = "viewer", None
|
| 358 |
-
|
| 359 |
-
def page(self, config, split, offset, length):
|
| 360 |
-
raise rows.ViewerError("The dataset generation failed", 502)
|
| 361 |
-
|
| 362 |
-
class FakeFiles:
|
| 363 |
-
kind = "files"
|
| 364 |
-
|
| 365 |
-
def __init__(self, spec, meta, tok):
|
| 366 |
-
pass
|
| 367 |
-
|
| 368 |
-
def splits(self):
|
| 369 |
-
return [{"config": "rlvr1", "split": "train"}]
|
| 370 |
-
|
| 371 |
-
def page(self, config, split, offset, length):
|
| 372 |
-
return {"rows": [(0, {"responses_create_params": {"input": "hi"}}, [])], "total": 1, "features": []}
|
| 373 |
-
|
| 374 |
-
monkeypatch.setattr(rows, "Files", FakeFiles)
|
| 375 |
-
rows._split_files.clear()
|
| 376 |
-
be, page = rows._first_page("org/blend", {"sha": "abc"}, BrokenViewer(), {"config": "rlvr1", "split": "train"})
|
| 377 |
-
assert be.kind == "files" and page["total"] == 1
|
| 378 |
-
be, _ = rows._first_page("org/blend", {"sha": "abc"}, BrokenViewer(), {"config": "rlvr1", "split": "train"}) # remembered
|
| 379 |
-
assert be.kind == "files"
|
| 380 |
-
with pytest.raises(rows.ViewerError): # a split the files don't have either: the viewer's error stands
|
| 381 |
-
rows._first_page("org/blend", {"sha": "abc"}, BrokenViewer(), {"config": "other", "split": "train"})
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
def test_blend_rows_are_pivots_only_when_they_carry_an_expert_step_and_placeholders_say_where_from():
|
| 385 |
-
plain = {"responses_create_params": {"input": [{"role": "user", "content": "q"}]}, "expected_action": None, "ref_message": None,
|
| 386 |
-
"agent_ref": {"type": "responses_api_agents", "name": "mcqa_simple_agent"}}
|
| 387 |
-
assert not nemogym.is_pivot(plain) and nemogym.is_pivot(pivot_row())
|
| 388 |
-
ph = {"question": "", "responses_create_params": {"input": [{"role": "user", "content": ""}]}, "expected_answer": "",
|
| 389 |
-
"agent_ref": {"type": "responses_api_agents", "name": "math_with_judge_simple_agent"},
|
| 390 |
-
"_hf_question_placeholder": {"mode": "canonical", "dataset": "Skywork/Skywork-OR1-RL-Data", "split": "math", "row": 246}}
|
| 391 |
-
view, card, *_ = everything(nemogym.NemoGym(), ph)
|
| 392 |
-
assert card["title"] == "A question from Skywork/Skywork-OR1-RL-Data (math, row 246)"
|
| 393 |
-
assert "fills it in from" in json.dumps(view["sections"][0]["body"])
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
def test_search_never_finds_a_row_through_its_withheld_answer(monkeypatch):
|
| 397 |
-
"""The dataset viewer's search matches every column; a hit found only through an answer is dropped, and the total
|
| 398 |
-
then says nothing about how many rows hold it. Rows read from files are searched without their answers too."""
|
| 399 |
-
from app.envs import rows, viewer
|
| 400 |
-
|
| 401 |
-
found = [(0, {"prompt": "add 2 and 3", "reward_model": {"ground_truth": "5"}}, []),
|
| 402 |
-
(1, {"prompt": "the answer is 5 apples", "reward_model": {"ground_truth": "7"}}, [])]
|
| 403 |
-
monkeypatch.setattr(viewer, "search", lambda *a, **k: {"rows": found, "total": 900, "features": []})
|
| 404 |
-
rows._broken.clear()
|
| 405 |
-
page, _ = rows.Viewer("o/ds", None).search("default", "train", "5", 0)
|
| 406 |
-
assert [i for i, _, _ in page["rows"]] == [1] and page["total"] == 1
|
| 407 |
-
page, _ = rows.Viewer("o/ds", None).search("default", "train", "apple", 0) # a stemmed word still counts as visible
|
| 408 |
-
assert [i for i, _, _ in page["rows"]] == [1]
|
| 409 |
-
|
| 410 |
-
files = rows.Files.__new__(rows.Files)
|
| 411 |
-
monkeypatch.setattr(rows.Files, "_scan", lambda self, c, s, o, match, what: ({"rows": [h for h in found if match(h[1])]}, None))
|
| 412 |
-
assert [i for i, _, _ in files.search("d", "train", "7", 0)[0]["rows"]] == []
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
def test_verifiers_v1_taskdata_system_prompt_is_part_of_the_conversation():
|
| 416 |
-
row = {"prompt": "Solve the task", "system_prompt": "Use the supplied tools", "answer": "PRIVATE_REFERENCE"}
|
| 417 |
-
view, _, _, _, text = everything(verifiers_rows.Verifiers(), row, tags=["verifiers"])
|
| 418 |
-
turns = next(s for s in view["sections"] if s["id"] == "task")["body"][0]["data"]["turns"]
|
| 419 |
-
assert [(t["role"], t["text"]) for t in turns] == [("system", "Use the supplied tools"), ("user", "Solve the task")]
|
| 420 |
-
assert "PRIVATE_REFERENCE" not in text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_harbor_spec.py
DELETED
|
@@ -1,340 +0,0 @@
|
|
| 1 |
-
"""Harbor's task.toml as Harbor 0.23 reads it (app/catalog.py), and what a task page shows of it (app/envs/harbor.py).
|
| 2 |
-
No network: each task is a folder written to a temp dir, indexed by catalog._row, and served through stubs.
|
| 3 |
-
|
| 4 |
-
What is checked: multi-step tasks read their instructions from steps/<name>/instruction.md; a grader in a container of
|
| 5 |
-
its own is said to run there, built from tests/Dockerfile; legacy `memory = "4G"` is converted; tasks that ask for a
|
| 6 |
-
GPU, a Windows container or no network for one phase can't run here (and say why); nested metadata and other tables are
|
| 7 |
-
kept; a model judge is a real model call, not a mention; healthchecks and MCP servers are shown; a step's solution and
|
| 8 |
-
the answers in its tests stay out.
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
from __future__ import annotations
|
| 12 |
-
|
| 13 |
-
import json
|
| 14 |
-
|
| 15 |
-
import pytest
|
| 16 |
-
|
| 17 |
-
from app import catalog
|
| 18 |
-
from app.envs import contract as c
|
| 19 |
-
from app.envs.harbor import HarborAdapter
|
| 20 |
-
|
| 21 |
-
TB = """schema_version = "1.4"
|
| 22 |
-
[task]
|
| 23 |
-
name = "org/{name}"
|
| 24 |
-
[metadata]
|
| 25 |
-
difficulty = "easy"
|
| 26 |
-
[verifier]
|
| 27 |
-
timeout_sec = 300
|
| 28 |
-
[agent]
|
| 29 |
-
timeout_sec = 600
|
| 30 |
-
[environment]
|
| 31 |
-
docker_image = "python:3.12-slim"
|
| 32 |
-
"""
|
| 33 |
-
TEST_SH = "#!/bin/bash\npytest /tests/test_outputs.py && echo 1 > /logs/verifier/reward.txt\n"
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def dump(x) -> str:
|
| 37 |
-
return json.dumps(x, ensure_ascii=False)
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
def build(tmp_path, files: dict[str, str], name: str = "t1"):
|
| 41 |
-
"""A task folder on disk, indexed the way build_index indexes it: (row, the pack's texts, file sizes, picks)."""
|
| 42 |
-
root = tmp_path / name
|
| 43 |
-
for rel, text in files.items():
|
| 44 |
-
p = root / rel
|
| 45 |
-
p.parent.mkdir(parents=True, exist_ok=True)
|
| 46 |
-
p.write_text(text)
|
| 47 |
-
sizes = {rel: len(text.encode()) for rel, text in files.items()}
|
| 48 |
-
picks = catalog._pick(sizes)
|
| 49 |
-
row, kept = catalog._row(0, name, tmp_path, sizes, picks, {})
|
| 50 |
-
return row, kept, sizes, picks
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
def view(monkeypatch, tmp_path, files: dict[str, str]):
|
| 54 |
-
"""The task page's view of a task folder, through HarborAdapter.task, with the index and the pack stubbed."""
|
| 55 |
-
row, kept, sizes, picks = build(tmp_path, files)
|
| 56 |
-
monkeypatch.setattr(catalog, "_task_row", lambda s, p, t=None: (row, {"sha": "abc", "info": {}}))
|
| 57 |
-
monkeypatch.setattr(catalog, "_task_texts", lambda s, sha, p, t: (sizes, kept, set(picks)))
|
| 58 |
-
monkeypatch.setattr(catalog, "_walked_root", lambda s, idx, p, t: ({}, []))
|
| 59 |
-
monkeypatch.setattr(catalog, "collection", lambda s: None)
|
| 60 |
-
adapter = HarborAdapter()
|
| 61 |
-
return adapter.task(c.Env("dataset", "org/ds", {"sha": "abc"}, adapter), "t1"), row, kept
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
def section(v, sid: str) -> dict:
|
| 65 |
-
return next(s for s in v["sections"] if s["id"] == sid)
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def rows_of(sec: dict) -> dict:
|
| 69 |
-
"""A section's kv rows as {label: value} (the first of a repeated label)."""
|
| 70 |
-
out: dict = {}
|
| 71 |
-
for b in sec["blocks"]:
|
| 72 |
-
if b["type"] == "kv":
|
| 73 |
-
for k, v in b["rows"]:
|
| 74 |
-
out.setdefault(k, v)
|
| 75 |
-
return out
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
# ── multi-step tasks ─────────────────────────────────────────────────────────
|
| 79 |
-
MULTI = """version = "1.0"
|
| 80 |
-
multi_step_reward_strategy = "final"
|
| 81 |
-
[environment]
|
| 82 |
-
docker_image = "python:3.12"
|
| 83 |
-
[[steps]]
|
| 84 |
-
name = "write"
|
| 85 |
-
min_reward = 0.5
|
| 86 |
-
[steps.agent]
|
| 87 |
-
timeout_sec = 120
|
| 88 |
-
[[steps]]
|
| 89 |
-
name = "extend"
|
| 90 |
-
"""
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
def test_multi_step_instructions_come_from_each_steps_folder(tmp_path, monkeypatch):
|
| 94 |
-
files = {"task.toml": MULTI, "tests/test.sh": TEST_SH,
|
| 95 |
-
"steps/write/instruction.md": "Implement greet(name) in /app/greeting.py.\n\nReturn a greeting.",
|
| 96 |
-
"steps/write/tests/test.sh": "echo 1 > /logs/verifier/reward.txt\n",
|
| 97 |
-
"steps/write/workdir/setup.sh": "mkdir -p /app\n",
|
| 98 |
-
"steps/extend/instruction.md": "Extend greet with an uppercase flag.",
|
| 99 |
-
"steps/extend/solution/solve.sh": "echo SECRET-STEP-SOLUTION\n"}
|
| 100 |
-
assert catalog._wanted("steps/write/instruction.md", 100) and catalog._wanted("steps/write/tests/test.sh", 100)
|
| 101 |
-
v, row, kept = view(monkeypatch, tmp_path, files)
|
| 102 |
-
assert row["steps"] == 2 and row["spec"]["step_names"] == ["write", "extend"]
|
| 103 |
-
assert row["spec"]["multi"] == {"strategy": "final", "min_reward": {"write": 0.5}}
|
| 104 |
-
assert row["title"].startswith("Implement greet(name)") # no instruction.md: the first step's
|
| 105 |
-
assert "steps/write/instruction.md" in kept and "steps/write/tests/test.sh" in kept
|
| 106 |
-
assert row["run"] == "image" and row["solution"] # a step's solution counts as the task's reference solution
|
| 107 |
-
steps = section(v, "steps")
|
| 108 |
-
text = dump(steps)
|
| 109 |
-
assert "Implement greet(name)" in text and "Extend greet with an uppercase flag." in text
|
| 110 |
-
assert "`tests/test.sh`" in text and "`setup.sh`" in text and "reward ≥ 0.5" in text
|
| 111 |
-
assert section(v, "task")["title"] == "The task, in 2 steps"
|
| 112 |
-
assert "the last step's reward" in dump(section(v, "config"))
|
| 113 |
-
assert "SECRET-STEP-SOLUTION" not in dump(v)
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
def test_an_inline_step_instruction_is_flagged(tmp_path, monkeypatch):
|
| 117 |
-
toml = 'version = "1.0"\n[environment]\ndocker_image = "x"\n[[steps]]\nname = "only"\ninstruction = "Do the thing."\n'
|
| 118 |
-
v, row, _ = view(monkeypatch, tmp_path, {"task.toml": toml, "tests/test.sh": TEST_SH})
|
| 119 |
-
text = dump(section(v, "steps"))
|
| 120 |
-
assert "Do the thing." in text and "no longer reads it" in text
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
# ── a grader in a container of its own ───────────────────────────────────────
|
| 124 |
-
SEPARATE = TB + """[verifier.environment]
|
| 125 |
-
cpus = 2
|
| 126 |
-
memory = "4G"
|
| 127 |
-
"""
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
def test_separate_verifier_is_said_to_run_in_its_own_container(tmp_path, monkeypatch):
|
| 131 |
-
toml = SEPARATE.format(name="sep").replace("[verifier]\n", '[verifier]\nenvironment_mode = "separate"\n')
|
| 132 |
-
files = {"task.toml": toml, "instruction.md": "Do it.", "tests/test.sh": TEST_SH,
|
| 133 |
-
"tests/Dockerfile": "FROM python:3.12-slim\nRUN pip install pytest\nCOPY . /tests\n"}
|
| 134 |
-
v, row, _ = view(monkeypatch, tmp_path, files)
|
| 135 |
-
sep = row["verifier"]["separate"]
|
| 136 |
-
assert sep["own"] and sep["dockerfile"] and sep["ok"] and not sep.get("image")
|
| 137 |
-
runs = rows_of(section(v, "grading"))["Runs"]
|
| 138 |
-
assert "separate container" in runs and "tests/Dockerfile" in runs and "same sandbox" not in runs
|
| 139 |
-
assert "4 GB memory" in rows_of(section(v, "config"))["Grader's container"]
|
| 140 |
-
assert ["Grader", "in its own container"] in v["glance"]
|
| 141 |
-
assert catalog.runnable(row, {})["ok"]
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
def test_separate_verifier_without_an_image_or_dockerfile_cant_run(tmp_path):
|
| 145 |
-
toml = TB.format(name="sep2").replace('docker_image = "python:3.12-slim"\n', "") + '[verifier.environment]\ncpus = 1\n'
|
| 146 |
-
row, *_ = build(tmp_path, {"task.toml": toml, "instruction.md": "x", "tests/test.sh": TEST_SH,
|
| 147 |
-
"environment/Dockerfile": "FROM python:3.12\n"})
|
| 148 |
-
r = catalog.runnable(row, {"environment/Dockerfile": "FROM python:3.12\n"})
|
| 149 |
-
assert not r["ok"] and "container of its own" in r["why"]
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
# ── what Harbor converts, and what an HF Sandbox can't do ────────────────────
|
| 153 |
-
def test_legacy_memory_and_storage_strings_are_converted(tmp_path, monkeypatch):
|
| 154 |
-
toml = TB.format(name="m").replace("[environment]\n", '[environment]\ncpus = 1\nmemory = "4G"\nstorage = "512M"\n')
|
| 155 |
-
v, row, _ = view(monkeypatch, tmp_path, {"task.toml": toml, "instruction.md": "x", "tests/test.sh": TEST_SH})
|
| 156 |
-
assert row["env"]["memory_mb"] == 4096 and row["env"]["storage_mb"] == 512
|
| 157 |
-
assert rows_of(section(v, "config"))["Resources"] == "1 CPU · 4 GB memory · 512 MB storage"
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
@pytest.mark.parametrize("patch,why", [
|
| 161 |
-
('[environment]\ngpus = 1\ngpu_types = ["L4"]\n', "GPU"),
|
| 162 |
-
('[environment]\nos = "Windows"\n', "Windows"),
|
| 163 |
-
('[environment]\ntpu = { type = "v6e", topology = "2x4" }\n', "TPU"),
|
| 164 |
-
])
|
| 165 |
-
def test_what_hf_sandboxes_cant_give_is_not_runnable(tmp_path, patch, why):
|
| 166 |
-
toml = TB.format(name="g").replace("[environment]\n", patch)
|
| 167 |
-
row, *_ = build(tmp_path, {"task.toml": toml, "instruction.md": "x", "tests/test.sh": TEST_SH, "tests/test.bat": "@echo off\n"})
|
| 168 |
-
r = catalog.runnable(row, {})
|
| 169 |
-
assert row["run"] is None and not r["ok"] and why in r["why"]
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
@pytest.mark.parametrize("section_,label", [("agent", "agent"), ("verifier", "grader")])
|
| 173 |
-
def test_a_phase_without_internet_is_not_runnable(tmp_path, monkeypatch, section_, label):
|
| 174 |
-
toml = TB.format(name="n").replace(f"[{section_}]\n", f'[{section_}]\nnetwork_mode = "no-network"\n')
|
| 175 |
-
v, row, _ = view(monkeypatch, tmp_path, {"task.toml": toml, "instruction.md": "x", "tests/test.sh": TEST_SH})
|
| 176 |
-
assert row["env"]["network"] is None and row["env"]["phases"] == {section_: "no-network"}
|
| 177 |
-
r = catalog.runnable(row, {})
|
| 178 |
-
assert not r["ok"] and f"the {label} asks for no internet access" in r["why"]
|
| 179 |
-
assert f"{label}: no internet" in rows_of(section(v, "config"))["Network"]
|
| 180 |
-
assert not v["_runnable"]["ok"]
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
def test_an_allowlist_lists_its_hosts(tmp_path, monkeypatch):
|
| 184 |
-
toml = TB.format(name="a").replace("[environment]\n", '[environment]\nnetwork_mode = "allowlist"\nallowed_hosts = ["pypi.org", "*.github.com"]\n')
|
| 185 |
-
v, row, _ = view(monkeypatch, tmp_path, {"task.toml": toml, "instruction.md": "x", "tests/test.sh": TEST_SH})
|
| 186 |
-
assert row["env"]["hosts"] == ["pypi.org", "*.github.com"] and not catalog.runnable(row, {})["ok"]
|
| 187 |
-
assert "`pypi.org`" in rows_of(section(v, "config"))["Allowed hosts"]
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
def test_a_task_toml_harbor_rejects_is_read_as_written_and_cant_run(tmp_path):
|
| 191 |
-
toml = '[task]\nname = "no-org-slash"\n[environment]\ndocker_image = "x"\nmemory = "2G"\n'
|
| 192 |
-
row, *_ = build(tmp_path, {"task.toml": toml, "instruction.md": "x", "tests/test.sh": TEST_SH})
|
| 193 |
-
assert "task.name" in row["spec"]["harbor_error"] and row["env"]["memory_mb"] == 2048
|
| 194 |
-
r = catalog.runnable(row, {})
|
| 195 |
-
assert not r["ok"] and "Harbor itself rejects" in r["why"]
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
# ── metadata ────────────────────────────────────────��────────────────────────
|
| 199 |
-
NESTED = TB + """[metadata.repo2env]
|
| 200 |
-
recipe = "tasksmith"
|
| 201 |
-
fail_to_pass_count = 14
|
| 202 |
-
gold_patch = "diff --git SECRET-PATCH"
|
| 203 |
-
[metadata.repo2env.evaluation]
|
| 204 |
-
status = "verified"
|
| 205 |
-
[metadata.expected_result]
|
| 206 |
-
value = "SECRET-NESTED"
|
| 207 |
-
[scoring]
|
| 208 |
-
difficulty_label = "hard"
|
| 209 |
-
"""
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
def test_nested_metadata_and_other_tables_are_kept_answers_left_out(tmp_path, monkeypatch):
|
| 213 |
-
v, row, kept = view(monkeypatch, tmp_path, {"task.toml": NESTED.format(name="nm"), "instruction.md": "x", "tests/test.sh": TEST_SH})
|
| 214 |
-
assert row["meta"]["repo2env.recipe"] == "tasksmith" and row["meta"]["repo2env.evaluation.status"] == "verified"
|
| 215 |
-
assert row["meta"]["scoring.difficulty_label"] == "hard"
|
| 216 |
-
assert "repo2env.gold_patch" in row["withheld"] and "expected_result" in row["withheld"]
|
| 217 |
-
assert not any("gold" in k or "expected" in k for k in row["meta"])
|
| 218 |
-
meta = dump(section(v, "metadata"))
|
| 219 |
-
assert "tasksmith" in meta and "verified" in meta and "hard" in meta
|
| 220 |
-
assert "SECRET" not in dump(v) and "SECRET" not in kept["task.toml"] # nor in the raw task.toml shown
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
def test_masked_toml_hides_multi_line_values_and_answer_tables():
|
| 224 |
-
out = catalog._masked_toml('[metadata]\nexpected = [\n "a1",\n "a2",\n]\ntags = [\n "x",\n]\n[metadata.gold]\nv = 42\n'
|
| 225 |
-
'[solution.env]\nFOO = "${FOO}"\n')
|
| 226 |
-
assert "a1" not in out and "42" not in out and '"x"' in out and 'FOO = "${FOO}"' in out
|
| 227 |
-
import tomllib
|
| 228 |
-
|
| 229 |
-
assert tomllib.loads(out)["metadata"]["gold"] == {"v": "‹withheld›"}
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
# ── a model judge is a model call ────────────────────────────────────────────
|
| 233 |
-
GRADER_CALLS = """import os
|
| 234 |
-
from openai import OpenAI
|
| 235 |
-
|
| 236 |
-
client = OpenAI(base_url=os.environ["JUDGE_URL"])
|
| 237 |
-
resp = client.chat.completions.create(model="gpt-4o", messages=[{"role": "user", "content": "grade"}])
|
| 238 |
-
"""
|
| 239 |
-
GRADER_MENTIONS = """# compare with an OpenAI-style judge later, maybe
|
| 240 |
-
def grade(answer: str, judge: bool = False) -> float:
|
| 241 |
-
llm_judge = None
|
| 242 |
-
return float(answer.strip() == open('/app/out.txt').read().strip())
|
| 243 |
-
"""
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
@pytest.mark.parametrize("grader,judge", [(GRADER_CALLS, True), (GRADER_MENTIONS, False)])
|
| 247 |
-
def test_judge_is_a_real_model_call_not_a_mention(tmp_path, grader, judge):
|
| 248 |
-
row, *_ = build(tmp_path, {"task.toml": TB.format(name="j"), "instruction.md": "x",
|
| 249 |
-
"tests/test.sh": "python3 /tests/grade.py > /logs/verifier/reward.txt\n", "tests/grade.py": grader})
|
| 250 |
-
assert row["verifier"]["judge"] is judge
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
def test_judge_from_the_graders_model_key():
|
| 254 |
-
toml = TB.format(name="k") + '[verifier.env]\nOPENAI_API_KEY = "${OPENAI_API_KEY}"\n'
|
| 255 |
-
assert catalog.verifier_kind("", "", toml_text=toml, sources={})["judge"]
|
| 256 |
-
assert not catalog.verifier_kind("", "", ["SOME_API_KEY"], toml_text=TB.format(name="k"), sources={})["judge"] # a key's name isn't a judge
|
| 257 |
-
js = "import OpenAI from 'openai';\nconst r = await client.responses.create({model: 'x', input: 'y'});\n"
|
| 258 |
-
assert catalog._calls_model(js) and not catalog._calls_model("import openai # unused\nprint('judge')\n")
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
# ── what the agent gets besides the instruction ──────────────────────────────
|
| 262 |
-
def test_healthcheck_mcp_servers_and_settings_are_shown(tmp_path, monkeypatch):
|
| 263 |
-
toml = TB.format(name="h").replace("[agent]\n", '[agent]\nuser = "learner"\n') + """workdir = "/testbed"
|
| 264 |
-
skills_dir = "/skills"
|
| 265 |
-
[environment.healthcheck]
|
| 266 |
-
command = "test -f /ready"
|
| 267 |
-
interval_sec = 2
|
| 268 |
-
retries = 5
|
| 269 |
-
[[environment.mcp_servers]]
|
| 270 |
-
name = "docs"
|
| 271 |
-
transport = "streamable-http"
|
| 272 |
-
url = "http://docs:8000/mcp"
|
| 273 |
-
[[verifier.collect]]
|
| 274 |
-
command = "pg_dump > /logs/db.sql"
|
| 275 |
-
service = "db"
|
| 276 |
-
[[artifacts]]
|
| 277 |
-
source = "/app/out"
|
| 278 |
-
exclude = ["*.pyc"]
|
| 279 |
-
"""
|
| 280 |
-
v, row, _ = view(monkeypatch, tmp_path, {"task.toml": toml, "instruction.md": "x", "tests/test.sh": TEST_SH, "trajectory.json": "{}"})
|
| 281 |
-
assert row["env"]["healthcheck"] and row["env"]["mcp"] == ["docs"] and row["env"]["workdir"] == "/testbed"
|
| 282 |
-
assert row["verifier"]["collect"] == 1 and row["spec"]["agent_user"] == "learner" and row["spec"]["artifacts"] == ["/app/out"]
|
| 283 |
-
cfg = rows_of(section(v, "config"))
|
| 284 |
-
assert "`test -f /ready`" in cfg["Healthcheck"] and "every 2 s" in cfg["Healthcheck"] and "5 failed tries" in cfg["Healthcheck"]
|
| 285 |
-
assert cfg["MCP server"].startswith("`docs` · streamable-http") and "`http://docs:8000/mcp`" in cfg["MCP server"]
|
| 286 |
-
assert cfg["Users"] == "agent as `learner`" and cfg["Working directory"] == "`/testbed`" and "`/skills`" in cfg["Skills"]
|
| 287 |
-
assert "`pg_dump > /logs/db.sql` in `db`" in cfg["Collect hooks"] and "`/app/out`" in cfg["Artifacts"]
|
| 288 |
-
assert cfg["Name"] == "`org/h`" and "Prior context" in cfg
|
| 289 |
-
assert any(b["type"] == "disclose" and b["label"].startswith("task.toml") for b in section(v, "config")["blocks"])
|
| 290 |
-
assert "trajectory.json" in dump(section(v, "task"))
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
def test_summary_facets_name_what_sets_tasks_apart(tmp_path, monkeypatch):
|
| 294 |
-
plain, *_ = build(tmp_path, {"task.toml": TB.format(name="p"), "instruction.md": "x", "tests/test.sh": TEST_SH}, "p")
|
| 295 |
-
multi, *_ = build(tmp_path, {"task.toml": MULTI, "tests/test.sh": TEST_SH, "steps/write/instruction.md": "a",
|
| 296 |
-
"steps/extend/instruction.md": "b"}, "m")
|
| 297 |
-
rows = [plain, multi]
|
| 298 |
-
catalog._group(rows)
|
| 299 |
-
for r in rows:
|
| 300 |
-
r.pop("_paras", None)
|
| 301 |
-
r["brief"] = ""
|
| 302 |
-
idx = {"tasks": rows, "summary": catalog.summarize(rows), "info": {}, "sha": "abc"}
|
| 303 |
-
monkeypatch.setattr(catalog, "index_status", lambda spec, token=None: {"state": "done", "index": idx})
|
| 304 |
-
adapter = HarborAdapter()
|
| 305 |
-
env = c.Env("dataset", "org/ds", {"sha": "abc"}, adapter)
|
| 306 |
-
assert "setup" in [f["key"] for f in adapter.summary(env)["facets"]]
|
| 307 |
-
cards = {x["ref"]: x for x in adapter.tasks(env, everything=True)["cards"]}
|
| 308 |
-
assert cards["m"]["facets"]["setup"] == ["Multi-step"] and cards["p"]["facets"]["setup"] == []
|
| 309 |
-
assert "2 steps" in cards["m"]["chips"]
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
# ── answers stay out of steps ────────────────────────────────────────────────
|
| 313 |
-
@pytest.mark.parametrize("rel,text,withheld", [
|
| 314 |
-
("steps/s1/solution/solve.sh", None, True),
|
| 315 |
-
("steps/s1/solution/notes.md", "anything", True),
|
| 316 |
-
("steps/s1/tests/test.sh", "pytest", False),
|
| 317 |
-
("steps/s1/tests/grade.py", "import json", False),
|
| 318 |
-
("steps/s1/tests/expected.json", None, True),
|
| 319 |
-
("steps/s1/tests/data.csv", None, True),
|
| 320 |
-
("steps/s1/tests/check.py", "GOLD = {'gold_answer': 42}", True),
|
| 321 |
-
("steps/s1/instruction.md", "Write it.", False),
|
| 322 |
-
])
|
| 323 |
-
def test_step_answers_are_withheld(rel, text, withheld):
|
| 324 |
-
assert catalog._withheld(rel, text) is withheld
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
def test_a_step_solution_is_never_served(tmp_path, monkeypatch):
|
| 328 |
-
files = {"task.toml": MULTI, "tests/test.sh": TEST_SH, "steps/write/instruction.md": "a", "steps/extend/instruction.md": "b",
|
| 329 |
-
"steps/write/tests/test.sh": 'EXPECTED="SECRET-42"\n[ "$(cat /app/x)" = "$EXPECTED" ] && echo 1 > /logs/verifier/reward.txt\n',
|
| 330 |
-
"steps/write/solution/solve.sh": "echo SECRET-SOLVE\n"}
|
| 331 |
-
row, kept, sizes, picks = build(tmp_path, files)
|
| 332 |
-
assert "steps/write/solution/solve.sh" not in kept and "SECRET-42" not in kept["steps/write/tests/test.sh"]
|
| 333 |
-
monkeypatch.setattr(catalog, "_task_row", lambda s, p, t=None: (row, {"sha": "abc", "info": {}}))
|
| 334 |
-
monkeypatch.setattr(catalog, "_task_texts", lambda s, sha, p, t: (sizes, kept, set(picks)))
|
| 335 |
-
monkeypatch.setattr(catalog, "_walked_root", lambda s, idx, p, t: ({}, []))
|
| 336 |
-
monkeypatch.setattr(catalog, "_fetch", lambda *a, **k: pytest.fail("a withheld file was fetched"))
|
| 337 |
-
got = catalog.task_file("org/ds", "t1", "steps/write/solution/solve.sh")
|
| 338 |
-
assert got.get("withheld") and "text" not in got
|
| 339 |
-
tree = {f["path"]: f["withheld"] for f in catalog.task("org/ds", "t1")["tree"]}
|
| 340 |
-
assert tree["steps/write/solution/solve.sh"] is True and tree["steps/write/instruction.md"] is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_judge.py
DELETED
|
@@ -1,99 +0,0 @@
|
|
| 1 |
-
"""A Harbor task's variables and model-graded tasks (app/judge.py), on the patterns real datasets use (MiMo's
|
| 2 |
-
conversions, skilltrainbench, data-agent, WildClawBench): what each variable gets in a rollout here, in which phase,
|
| 3 |
-
and that nothing the agent sees is a key of ours."""
|
| 4 |
-
|
| 5 |
-
from __future__ import annotations
|
| 6 |
-
|
| 7 |
-
from app import judge
|
| 8 |
-
|
| 9 |
-
MIMO_GENERAL = '''
|
| 10 |
-
[verifier.env]
|
| 11 |
-
GA_JUDGE_KEY = "${HF_TOKEN}"
|
| 12 |
-
GA_JUDGE_URL = "${MIMO_JUDGE_URL:-https://router.huggingface.co/v1}"
|
| 13 |
-
GA_JUDGE_MODEL = "${MIMO_TEXT_JUDGE:-thinkingmachines/Inkling}"
|
| 14 |
-
VERIFY_DETERMINISTIC = "1"
|
| 15 |
-
'''
|
| 16 |
-
MIMO_WEBDEV = '''
|
| 17 |
-
[verifier.env]
|
| 18 |
-
WEBDEV_JUDGE_KEY = "${HF_TOKEN}"
|
| 19 |
-
WEBDEV_JUDGE_URL = "${MIMO_JUDGE_URL:-https://router.huggingface.co/v1}"
|
| 20 |
-
WEBDEV_JUDGE_MODEL = "${MIMO_VISION_JUDGE:-meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8}"
|
| 21 |
-
'''
|
| 22 |
-
HEALTHBENCH = '''
|
| 23 |
-
[verifier.env]
|
| 24 |
-
STBENCH_JUDGE_BASE_URL = "${STBENCH_JUDGE_BASE_URL}"
|
| 25 |
-
STBENCH_JUDGE_API_KEY = "${STBENCH_JUDGE_API_KEY}"
|
| 26 |
-
STBENCH_JUDGE_MODEL = "${STBENCH_JUDGE_MODEL}"
|
| 27 |
-
STBENCH_JUDGE_TEMPERATURE = "${STBENCH_JUDGE_TEMPERATURE}"
|
| 28 |
-
'''
|
| 29 |
-
HLE = '''
|
| 30 |
-
[verifier.env]
|
| 31 |
-
OPENAI_API_KEY = "${OPENAI_API_KEY}"
|
| 32 |
-
OPENAI_BASE_URL = "${OPENAI_BASE_URL:-}"
|
| 33 |
-
JUDGE_MODEL = "o3-mini-2025-01-31"
|
| 34 |
-
'''
|
| 35 |
-
DATA_AGENT = '''
|
| 36 |
-
[environment.env]
|
| 37 |
-
HF_TOKEN = "${HF_TOKEN}"
|
| 38 |
-
'''
|
| 39 |
-
NEEDS_SERVICE = '''
|
| 40 |
-
[environment.env]
|
| 41 |
-
DATABASE_URL = "${DATABASE_URL}"
|
| 42 |
-
'''
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
def test_a_model_graded_task_gets_the_relay_in_its_grading_phase_only():
|
| 46 |
-
p = judge.plan(MIMO_GENERAL)
|
| 47 |
-
assert p["judge"] == {"requested": "thinkingmachines/Inkling", "vision": False, "anthropic": False, "keys": ["GA_JUDGE_KEY", "GA_JUDGE_URL"]}
|
| 48 |
-
assert p["missing"] == [] and p["agent_keys"] == []
|
| 49 |
-
b = judge.bindings(p, relay="https://x.hf.space/capture", capability="jcap", judge="thinkingmachines/Inkling")
|
| 50 |
-
assert b["verifier"]["GA_JUDGE_KEY"] == "jcap" and b["verifier"]["HF_TOKEN"] == "jcap"
|
| 51 |
-
assert b["verifier"]["GA_JUDGE_URL"] == "https://x.hf.space/capture/v1" # never the real router: the relay holds the token
|
| 52 |
-
assert b["verifier"]["GA_JUDGE_MODEL"] == "thinkingmachines/Inkling"
|
| 53 |
-
assert "jcap" not in b["environment"].values()
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def test_a_vision_grader_asks_for_a_vision_judge():
|
| 57 |
-
assert judge.plan(MIMO_WEBDEV)["judge"]["vision"] is True
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def test_a_judge_without_defaults_gets_the_picked_model_and_deterministic_settings():
|
| 61 |
-
p = judge.plan(HEALTHBENCH)
|
| 62 |
-
assert p["judge"] and p["missing"] == []
|
| 63 |
-
b = judge.bindings(p, relay="https://r", capability="c", judge="openai/gpt-oss-120b")["verifier"]
|
| 64 |
-
assert b["STBENCH_JUDGE_MODEL"] == "openai/gpt-oss-120b" and b["STBENCH_JUDGE_TEMPERATURE"] == "0"
|
| 65 |
-
assert b["STBENCH_JUDGE_BASE_URL"] == "https://r/v1" and b["STBENCH_JUDGE_API_KEY"] == "c"
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
def test_an_openai_client_with_an_empty_default_url_is_pointed_at_the_relay():
|
| 69 |
-
p = judge.plan(HLE)
|
| 70 |
-
assert p["judge"]["requested"] == "o3-mini-2025-01-31"
|
| 71 |
-
b = judge.bindings(p, relay="https://r", capability="c", judge="deepseek-ai/DeepSeek-V4-Pro")["verifier"]
|
| 72 |
-
assert b["OPENAI_BASE_URL"] == "https://r/v1" and b["OPENAI_API_KEY"] == "c" # not api.openai.com with our key
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def test_a_key_the_agent_would_see_is_left_empty():
|
| 76 |
-
p = judge.plan(DATA_AGENT)
|
| 77 |
-
assert p["judge"] is None and p["agent_keys"] == ["HF_TOKEN"] and p["missing"] == []
|
| 78 |
-
assert judge.bindings(p)["environment"]["HF_TOKEN"] == ""
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def test_a_variable_nothing_here_provides_stops_the_task():
|
| 82 |
-
p = judge.plan(NEEDS_SERVICE)
|
| 83 |
-
assert p["missing"] == ["DATABASE_URL"]
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def test_without_a_judge_nothing_is_bound_to_a_relay():
|
| 87 |
-
b = judge.bindings(judge.plan(MIMO_GENERAL))
|
| 88 |
-
assert b["verifier"]["GA_JUDGE_KEY"] == "" and "GA_JUDGE_URL" not in b["verifier"] # no capability: an empty key, the task's defaults
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def test_the_table_says_how_each_variable_is_filled():
|
| 92 |
-
rows = dict(judge.table(judge.plan(MIMO_GENERAL)))
|
| 93 |
-
assert any("judge relay" in v for v in rows.values())
|
| 94 |
-
assert not any("hf_" in v for v in rows.values())
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
def test_a_broken_toml_is_not_a_crash():
|
| 98 |
-
p = judge.plan("this is = = not toml")
|
| 99 |
-
assert p["invalid"] and p["vars"] == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_live.py
DELETED
|
@@ -1,409 +0,0 @@
|
|
| 1 |
-
"""Live Spaces and the MCP bridge (no network: the Hub and the Spaces are stubbed).
|
| 2 |
-
|
| 3 |
-
What is checked: the explorer only ever talks to a Space's own hf.space host (never a redirect elsewhere, never a
|
| 4 |
-
private Space); waking and restarting do only what they should; playground sessions belong to whoever started them
|
| 5 |
-
and are capped; answers stay out of the Task API browser; the bridge speaks MCP (handshake, notifications, errors,
|
| 6 |
-
sessions), refuses cross-origin pages, and turns servers without tools into reset/step/state tools.
|
| 7 |
-
"""
|
| 8 |
-
|
| 9 |
-
from __future__ import annotations
|
| 10 |
-
|
| 11 |
-
import json
|
| 12 |
-
import time
|
| 13 |
-
from types import SimpleNamespace
|
| 14 |
-
|
| 15 |
-
import httpx
|
| 16 |
-
import pytest
|
| 17 |
-
from fastapi.testclient import TestClient
|
| 18 |
-
|
| 19 |
-
from app import catalog, main, mcp_bridge, spaces_live as live
|
| 20 |
-
|
| 21 |
-
client = TestClient(main.app, base_url="https://testserver")
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def space_info(stage="RUNNING", sub="org-env", private=False, base_path=None):
|
| 25 |
-
card = SimpleNamespace(to_dict=lambda: ({"base_path": base_path} if base_path else {}))
|
| 26 |
-
return SimpleNamespace(private=private, card_data=card, subdomain=sub, sdk="docker", tags=["openenv"],
|
| 27 |
-
runtime=SimpleNamespace(stage=stage, hardware="cpu-basic"))
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
@pytest.fixture(autouse=True)
|
| 31 |
-
def fresh(monkeypatch):
|
| 32 |
-
catalog._memo.clear()
|
| 33 |
-
live._sessions.clear()
|
| 34 |
-
live._woken.clear()
|
| 35 |
-
mcp_bridge._hits.clear()
|
| 36 |
-
mcp_bridge._clients.clear()
|
| 37 |
-
yield
|
| 38 |
-
for s in list(live._sessions.values()):
|
| 39 |
-
s.ws = None
|
| 40 |
-
live._sessions.clear()
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def stub_hub(monkeypatch, **kw):
|
| 44 |
-
monkeypatch.setattr(catalog, "_api", lambda token=None: SimpleNamespace(space_info=lambda spec, expand=None: space_info(**kw)))
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
# ── the Space's record ───────────────────────────────────────────────────────
|
| 48 |
-
def test_only_the_spaces_own_hf_space_host_is_used(monkeypatch):
|
| 49 |
-
stub_hub(monkeypatch, sub="evil.example.com/x")
|
| 50 |
-
assert live.record("org/env")["host"] == ""
|
| 51 |
-
catalog._memo.clear()
|
| 52 |
-
stub_hub(monkeypatch, sub="org-env")
|
| 53 |
-
assert live.record("org/env")["host"] == "https://org-env.hf.space"
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def test_private_spaces_are_refused(monkeypatch):
|
| 57 |
-
stub_hub(monkeypatch, private=True)
|
| 58 |
-
with pytest.raises(PermissionError):
|
| 59 |
-
live.record("org/env")
|
| 60 |
-
assert client.get("/api/spaces/org/env/live").status_code == 404
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def test_base_path_must_be_a_plain_path(monkeypatch):
|
| 64 |
-
stub_hub(monkeypatch, base_path="//evil.com")
|
| 65 |
-
assert live.record("org/env")["base_path"] is None
|
| 66 |
-
catalog._memo.clear()
|
| 67 |
-
stub_hub(monkeypatch, base_path="/web")
|
| 68 |
-
assert live.record("org/env")["base_path"] == "/web"
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def test_bad_space_ids_are_refused():
|
| 72 |
-
for bad in ("../x/y", "a/b/c"):
|
| 73 |
-
assert client.get(f"/api/spaces/{bad}/live").status_code in (400, 404)
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def test_redirects_off_the_space_are_never_followed(monkeypatch):
|
| 77 |
-
stub_hub(monkeypatch)
|
| 78 |
-
rec = live.record("org/env")
|
| 79 |
-
seen = []
|
| 80 |
-
|
| 81 |
-
def fake_get(url, **kw):
|
| 82 |
-
seen.append(url)
|
| 83 |
-
if url.endswith("/web"):
|
| 84 |
-
return httpx.Response(302, headers={"location": "http://169.254.169.254/latest/meta-data"}, request=httpx.Request("GET", url))
|
| 85 |
-
if url.endswith("/start-page"):
|
| 86 |
-
return httpx.Response(302, headers={"location": "/web/"}, request=httpx.Request("GET", url))
|
| 87 |
-
return httpx.Response(200, headers={"content-type": "text/html"}, text="<html>", request=httpx.Request("GET", url))
|
| 88 |
-
|
| 89 |
-
monkeypatch.setattr(live._client, "get", fake_get)
|
| 90 |
-
assert live._get_here(rec, "/web") is None # off-host: dropped, not fetched
|
| 91 |
-
assert not any("169.254" in u for u in seen)
|
| 92 |
-
r = live._get_here(rec, "/start-page") # on-host: followed
|
| 93 |
-
assert r is not None and r.url.path == "/web/"
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def test_hub_dataset_names_map_back_safely():
|
| 97 |
-
assert live.hub_dataset("/data/FineEnvs__MiMo-V2.6-RL-harbor-terminal") == "FineEnvs/MiMo-V2.6-RL-harbor-terminal"
|
| 98 |
-
assert live.hub_dataset("/app/prepared/datasets/train") is None
|
| 99 |
-
assert live.hub_dataset("/data/a__b__c") is None
|
| 100 |
-
assert live.hub_dataset("/data/__x") is None
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
# ── wake and restart ─────────────────────────────────────────────────────────
|
| 104 |
-
def test_wake_only_sleeping_spaces_and_at_most_once_a_minute(monkeypatch):
|
| 105 |
-
calls = []
|
| 106 |
-
monkeypatch.setattr(live.httpx, "post", lambda url, **kw: calls.append(url) or httpx.Response(302))
|
| 107 |
-
stub_hub(monkeypatch, stage="SLEEPING")
|
| 108 |
-
assert live.wake("org/env")["woken"] is True
|
| 109 |
-
assert live.wake("org/env")["woken"] is True # again within a minute: not sent again
|
| 110 |
-
assert calls == ["https://huggingface.co/spaces/org/env/start"]
|
| 111 |
-
catalog._memo.clear()
|
| 112 |
-
stub_hub(monkeypatch, stage="RUNTIME_ERROR")
|
| 113 |
-
with pytest.raises(live.SpaceError):
|
| 114 |
-
live.wake("org/env")
|
| 115 |
-
catalog._memo.clear()
|
| 116 |
-
stub_hub(monkeypatch, stage="RUNNING")
|
| 117 |
-
assert live.wake("org/env") == {"stage": "RUNNING", "woken": False}
|
| 118 |
-
assert len(calls) == 1
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
def test_restart_needs_a_signed_in_visitor():
|
| 122 |
-
assert client.post("/api/spaces/org/env/restart", json={}).status_code == 401
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def test_restart_without_write_access_says_so(monkeypatch):
|
| 126 |
-
from huggingface_hub.errors import HfHubHTTPError
|
| 127 |
-
|
| 128 |
-
def refuse(_spec):
|
| 129 |
-
raise HfHubHTTPError("no", response=httpx.Response(403, request=httpx.Request("POST", "https://huggingface.co")))
|
| 130 |
-
|
| 131 |
-
space = space_info(stage="RUNTIME_ERROR")
|
| 132 |
-
monkeypatch.setattr(catalog, "_api", lambda token=None: SimpleNamespace(space_info=lambda spec, expand=None: space, restart_space=refuse))
|
| 133 |
-
with pytest.raises(live.SpaceError) as e:
|
| 134 |
-
live.restart("org/env", "hf_x")
|
| 135 |
-
assert e.value.status == 403
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
def test_wake_and_play_refuse_cross_site_posts():
|
| 139 |
-
for path, body in (("/api/spaces/org/env/wake", {}), ("/api/spaces/org/env/play", {"op": "start"})):
|
| 140 |
-
r = client.post(path, json=body, headers={"Origin": "https://evil.example"})
|
| 141 |
-
assert r.status_code == 403
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
# ── the Task API browser ─────────────────────────────────────────────────────
|
| 145 |
-
def test_task_api_leaves_answers_out(monkeypatch):
|
| 146 |
-
stub_hub(monkeypatch)
|
| 147 |
-
monkeypatch.setattr(live, "probe", lambda spec, fresh=False: {"running": True, "task_api": {"env": "e", "splits": []}})
|
| 148 |
-
|
| 149 |
-
def fake_post(url, json=None, **kw):
|
| 150 |
-
task = {"task_id": "t1", "prompt": "find x", "ground_truth": "42", "meta": {"expected_output": "x", "level": 2},
|
| 151 |
-
"target_latex": "a^2", "target_language": "fr", "options": [{"answer": "b", "text": "B"}]}
|
| 152 |
-
body = {"tasks": [task]} if url.endswith("/task_range") else {"task": task}
|
| 153 |
-
return httpx.Response(200, json=body, request=httpx.Request("POST", url))
|
| 154 |
-
|
| 155 |
-
monkeypatch.setattr(live._client, "post", fake_post)
|
| 156 |
-
for out in (live.tasks("org/env", "train", 0, 999)["tasks"][0], live.task("org/env", "train", 0)["task"]):
|
| 157 |
-
text = json.dumps(out)
|
| 158 |
-
assert "42" not in text and "expected_output" not in text and "a^2" not in text and '"answer"' not in text
|
| 159 |
-
assert out["prompt"] == "find x" and out["target_language"] == "fr"
|
| 160 |
-
assert live.tasks("org/env", "train", 0, 999)["stop"] == 50 # a page is at most 50 tasks
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
# ── playground sessions ──────────────────────────────────────────────────────
|
| 164 |
-
def running(monkeypatch):
|
| 165 |
-
stub_hub(monkeypatch)
|
| 166 |
-
monkeypatch.setattr(live, "probe", lambda spec, fresh=False: {"running": True, "openenv": True, "step_api": True, "mcp": None})
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
def test_sessions_belong_to_who_started_them(monkeypatch):
|
| 170 |
-
running(monkeypatch)
|
| 171 |
-
sid = live.start("org/env", "web:1.1.1.1")["session"]
|
| 172 |
-
with pytest.raises(live.SpaceError) as e:
|
| 173 |
-
live.act(sid, "web:2.2.2.2", "state")
|
| 174 |
-
assert e.value.status == 410
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
def test_sessions_are_capped_per_owner_and_in_total(monkeypatch):
|
| 178 |
-
running(monkeypatch)
|
| 179 |
-
ids = [live.start("org/env", "web:a")["session"] for _ in range(live.PER_OWNER + 2)]
|
| 180 |
-
mine = [s for s in live._sessions.values() if s.owner == "web:a"]
|
| 181 |
-
assert len(mine) == live.PER_OWNER and ids[-1] in live._sessions and ids[0] not in live._sessions
|
| 182 |
-
monkeypatch.setattr(live, "TOTAL", len(live._sessions))
|
| 183 |
-
with pytest.raises(live.SpaceError) as e:
|
| 184 |
-
live.start("org/env", "web:b")
|
| 185 |
-
assert e.value.status == 429
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
def test_idle_sessions_end(monkeypatch):
|
| 189 |
-
running(monkeypatch)
|
| 190 |
-
sid = live.start("org/env", "web:a")["session"]
|
| 191 |
-
live._sessions[sid].used = time.time() - live.IDLE - 1
|
| 192 |
-
with pytest.raises(live.SpaceError):
|
| 193 |
-
live.act(sid, "web:a", "state")
|
| 194 |
-
assert sid not in live._sessions
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
def test_play_rejects_bad_operations_and_tool_names(monkeypatch):
|
| 198 |
-
running(monkeypatch)
|
| 199 |
-
sid = live.start("org/env", "web:a")["session"]
|
| 200 |
-
with pytest.raises(live.SpaceError):
|
| 201 |
-
live.act(sid, "web:a", "rm -rf")
|
| 202 |
-
with pytest.raises(live.SpaceError):
|
| 203 |
-
live.act(sid, "web:a", "call", {"name": "../../x y"})
|
| 204 |
-
r = client.post("/api/spaces/org/env/play", json={"op": "nope"})
|
| 205 |
-
assert r.status_code == 422
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
def test_play_sessions_do_not_cross_spaces(monkeypatch):
|
| 209 |
-
running(monkeypatch)
|
| 210 |
-
r = client.post("/api/spaces/org/env/play", json={"op": "start"})
|
| 211 |
-
sid = r.json()["session"]
|
| 212 |
-
r = client.post("/api/spaces/org/other/play", json={"op": "state", "session": sid})
|
| 213 |
-
assert r.status_code == 400
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
def test_websocket_replies_become_results_and_errors(monkeypatch):
|
| 217 |
-
running(monkeypatch)
|
| 218 |
-
sent = []
|
| 219 |
-
|
| 220 |
-
class FakeWS:
|
| 221 |
-
def __init__(self, replies):
|
| 222 |
-
self.replies = list(replies)
|
| 223 |
-
|
| 224 |
-
def send(self, m):
|
| 225 |
-
sent.append(json.loads(m))
|
| 226 |
-
|
| 227 |
-
def recv(self, timeout=None):
|
| 228 |
-
return json.dumps(self.replies.pop(0))
|
| 229 |
-
|
| 230 |
-
def close(self):
|
| 231 |
-
pass
|
| 232 |
-
|
| 233 |
-
sid = live.start("org/env", "web:a")["session"]
|
| 234 |
-
live._sessions[sid].ws = FakeWS([{"type": "observation", "data": {"observation": {"x": 1}, "reward": 0.5, "done": True}},
|
| 235 |
-
{"type": "error", "data": {"message": "bad action"}},
|
| 236 |
-
{"type": "mcp", "data": {"jsonrpc": "2.0", "id": 1, "result": {"content": [{"type": "text", "text": "ok"}]}}}])
|
| 237 |
-
out = live.act(sid, "web:a", "reset", {"seed": 1})
|
| 238 |
-
assert out["result"]["reward"] == 0.5 and sent[0] == {"type": "reset", "data": {"seed": 1}}
|
| 239 |
-
with pytest.raises(live.SpaceError) as e:
|
| 240 |
-
live.act(sid, "web:a", "step", {"a": 1})
|
| 241 |
-
assert "bad action" in str(e.value)
|
| 242 |
-
out = live.act(sid, "web:a", "call", {"name": "look", "arguments": {"h": 1}})
|
| 243 |
-
assert out["result"]["content"][0]["text"] == "ok" and sent[-1]["type"] == "mcp" and sent[-1]["data"]["method"] == "tools/call"
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
def test_images_in_observations_are_found():
|
| 247 |
-
import base64
|
| 248 |
-
|
| 249 |
-
png = base64.b64encode(b"\x89PNG" + b"0" * 400).decode()
|
| 250 |
-
parts = live.image_parts({"observation": {"image_base64": png, "image_format": "png", "text": "hi"}})
|
| 251 |
-
assert parts and parts[0][0] == "image/png"
|
| 252 |
-
assert live.image_parts({"observation": {"text": "x" * 500}}) == []
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
# ── the MCP bridge ───────────────────────────────────────────────────────────
|
| 256 |
-
INFO = {"running": True, "stage": "RUNNING", "openenv": True, "step_api": True, "mcp": None,
|
| 257 |
-
"schema": {"action": {"type": "object", "properties": {"latex": {"type": "string"}, "metadata": {"type": "object"}}}},
|
| 258 |
-
"task_api": {"env": "e", "splits": [{"name": "train", "num_tasks": 3}]}}
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
def rpc(method, params=None, rid=1, sid=None, **headers):
|
| 262 |
-
h = {"Content-Type": "application/json", **({"Mcp-Session-Id": sid} if sid else {}), **headers}
|
| 263 |
-
body = {"jsonrpc": "2.0", "method": method, **({"id": rid} if rid is not None else {}), **({"params": params} if params is not None else {})}
|
| 264 |
-
return client.post("/mcp/org/env", content=json.dumps(body), headers=h)
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
@pytest.fixture
|
| 268 |
-
def bridge(monkeypatch):
|
| 269 |
-
stub_hub(monkeypatch)
|
| 270 |
-
monkeypatch.setattr(live, "probe", lambda spec, fresh=False: INFO)
|
| 271 |
-
calls = []
|
| 272 |
-
monkeypatch.setattr(live, "act", lambda sid, owner, op, data=None: calls.append((op, data)) or {"result": {"observation": {"ok": 1}, "reward": 1.0}})
|
| 273 |
-
return calls
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
def test_bridge_handshake_and_session(bridge):
|
| 277 |
-
r = rpc("initialize", {"protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "t", "version": "1"}})
|
| 278 |
-
assert r.status_code == 200 and r.headers["mcp-session-id"]
|
| 279 |
-
res = r.json()["result"]
|
| 280 |
-
assert res["protocolVersion"] == "2025-06-18" and res["capabilities"]["tools"] == {"listChanged": False}
|
| 281 |
-
assert rpc("initialize", {"protocolVersion": "1999-01-01"}).json()["result"]["protocolVersion"] == mcp_bridge.PROTOCOLS[0]
|
| 282 |
-
assert rpc("notifications/initialized", rid=None).status_code == 202
|
| 283 |
-
assert rpc("ping").json()["result"] == {}
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
def test_bridge_offers_episode_and_task_tools_for_servers_without_tools(bridge):
|
| 287 |
-
tools = {t["name"]: t for t in rpc("tools/list").json()["result"]["tools"]}
|
| 288 |
-
assert set(tools) == {"reset", "step", "state", "list_splits", "list_tasks", "get_task"}
|
| 289 |
-
assert set(tools["step"]["inputSchema"]["properties"]) == {"latex"} # the action schema, metadata dropped
|
| 290 |
-
r = rpc("tools/call", {"name": "step", "arguments": {"latex": "x^2"}}).json()["result"]
|
| 291 |
-
assert bridge[-1] == ("step", {"latex": "x^2"}) and "content" in r and not r.get("isError")
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
def test_bridge_tool_errors_are_results_the_agent_can_read(bridge, monkeypatch):
|
| 295 |
-
def boom(*a, **k):
|
| 296 |
-
raise live.SpaceError("the Space didn't answer within 180 s", 504)
|
| 297 |
-
|
| 298 |
-
monkeypatch.setattr(live, "act", boom)
|
| 299 |
-
r = rpc("tools/call", {"name": "step", "arguments": {}}).json()
|
| 300 |
-
assert r["result"]["isError"] is True and "180 s" in r["result"]["content"][0]["text"]
|
| 301 |
-
assert rpc("tools/call", {"name": "nope", "arguments": {}}).json()["result"]["isError"] is True
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
def test_bridge_rejects_what_it_should(bridge):
|
| 305 |
-
assert rpc("resources/read").json()["error"]["code"] == -32601
|
| 306 |
-
assert client.post("/mcp/org/env", content="{nope", headers={"Content-Type": "application/json"}).status_code == 400
|
| 307 |
-
assert client.post("/mcp/org/env", content=json.dumps([{"jsonrpc": "2.0", "id": 1, "method": "ping"}])).status_code == 400
|
| 308 |
-
assert client.post("/mcp/org/env", content=json.dumps({"id": 1, "method": "ping"})).status_code == 400
|
| 309 |
-
assert rpc("ping", **{"Origin": "https://evil.example"}).status_code == 403
|
| 310 |
-
assert client.post("/mcp/org/env", content="x" * (mcp_bridge.MAX_BODY + 1)).status_code == 413
|
| 311 |
-
assert client.get("/mcp/org/env").status_code == 405
|
| 312 |
-
assert client.delete("/mcp/org/env").status_code == 204
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
def test_bridge_is_rate_limited(bridge, monkeypatch):
|
| 316 |
-
monkeypatch.setattr(mcp_bridge, "RATE", (5, 60))
|
| 317 |
-
codes = [rpc("ping").status_code for _ in range(8)]
|
| 318 |
-
assert codes[:5] == [200] * 5 and 429 in codes[5:]
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
def test_bridge_wakes_a_sleeping_space_instead_of_failing(monkeypatch):
|
| 322 |
-
stub_hub(monkeypatch, stage="SLEEPING")
|
| 323 |
-
monkeypatch.setattr(live, "probe", lambda spec, fresh=False: {"running": False, "stage": "SLEEPING"})
|
| 324 |
-
woke = []
|
| 325 |
-
monkeypatch.setattr(live, "wake", lambda spec: woke.append(spec) or {"stage": "APP_STARTING"})
|
| 326 |
-
r = rpc("tools/list").json()
|
| 327 |
-
assert r["error"]["code"] == -32002 and woke == ["org/env"]
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
def test_bridge_passes_through_mcp_content_and_drops_output_schemas(monkeypatch):
|
| 331 |
-
stub_hub(monkeypatch)
|
| 332 |
-
info = {**INFO, "mcp": [{"name": "look", "inputSchema": {"type": "object"}, "outputSchema": {"type": "object"}}]}
|
| 333 |
-
monkeypatch.setattr(live, "probe", lambda spec, fresh=False: info)
|
| 334 |
-
monkeypatch.setattr(live, "act", lambda *a, **k: {"result": {"content": [{"type": "text", "text": "hi", "annotations": None, "meta": None}],
|
| 335 |
-
"structured_content": {"result": "hi"}, "is_error": False}})
|
| 336 |
-
tools = rpc("tools/list").json()["result"]["tools"]
|
| 337 |
-
assert all("outputSchema" not in t for t in tools)
|
| 338 |
-
assert {t["name"] for t in tools} >= {"look", "reset", "state"} and "step" not in {t["name"] for t in tools}
|
| 339 |
-
r = rpc("tools/call", {"name": "look", "arguments": {}}).json()["result"]
|
| 340 |
-
assert r["content"] == [{"type": "text", "text": "hi"}] and r["structuredContent"] == {"result": "hi"} and "isError" not in r
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
# ── a server's own routes (NeMo Gym, ORS, custom) ────────────────────────────
|
| 344 |
-
OPENAPI = {"paths": {
|
| 345 |
-
"/guess": {"post": {"summary": "Guess", "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/G"}}}}}},
|
| 346 |
-
"/tasks/{task_id}": {"get": {"summary": "A task"}},
|
| 347 |
-
"//evil.example/x": {"get": {}}, "/a/../b": {"get": {}}, "/q?x=1": {"get": {}}, "/web/x": {"get": {}}, "/reset": {"post": {}},
|
| 348 |
-
"/self": {"post": {"requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/S"}}}}}},
|
| 349 |
-
}, "components": {"schemas": {"G": {"type": "object", "properties": {"word": {"type": "string", "title": "Word"}}, "required": ["word"]},
|
| 350 |
-
"S": {"type": "object", "properties": {"next": {"$ref": "#/components/schemas/S"}}}}}}
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
def test_routes_are_plain_paths_on_non_openenv_servers_only():
|
| 354 |
-
routes = live._api_routes(OPENAPI, openenv=False)
|
| 355 |
-
assert {(r["method"], r["path"]) for r in routes} == {("POST", "/guess"), ("GET", "/tasks/{task_id}"), ("POST", "/self"), ("POST", "/reset")}
|
| 356 |
-
guess = next(r for r in routes if r["path"] == "/guess")
|
| 357 |
-
assert guess["schema"]["properties"]["word"] == {"type": "string", "title": "Word"} and guess["schema"]["required"] == ["word"]
|
| 358 |
-
assert next(r for r in routes if r["path"] == "/tasks/{task_id}")["params"] == ["task_id"]
|
| 359 |
-
assert live._api_routes(OPENAPI, openenv=True) == [] # an OpenEnv server is used through reset/step and tools
|
| 360 |
-
json.dumps(routes) # a self-referencing schema is cut, not looped
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
def test_route_parameters_cannot_leave_the_route():
|
| 364 |
-
info = {"api": live._api_routes(OPENAPI, openenv=False)}
|
| 365 |
-
assert live.route_url(info, "GET", "/tasks/{task_id}", {"task_id": "t-1.2"}) == "/tasks/t-1.2"
|
| 366 |
-
for bad in ("../admin", "a/b", "x y", "", "%2e%2e"):
|
| 367 |
-
with pytest.raises(live.SpaceError):
|
| 368 |
-
live.route_url(info, "GET", "/tasks/{task_id}", {"task_id": bad})
|
| 369 |
-
with pytest.raises(live.SpaceError):
|
| 370 |
-
live.route_url(info, "POST", "/not/published", {})
|
| 371 |
-
with pytest.raises(live.SpaceError):
|
| 372 |
-
live.route_url(info, "DELETE", "/guess", {})
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
def test_route_calls_keep_their_cookies_and_withhold_answers_from_gets(monkeypatch):
|
| 376 |
-
stub_hub(monkeypatch)
|
| 377 |
-
monkeypatch.setattr(live, "probe", lambda spec, fresh=False: {"running": True, "openenv": False, "api": live._api_routes(OPENAPI, openenv=False)})
|
| 378 |
-
seen = []
|
| 379 |
-
|
| 380 |
-
def handler(request: httpx.Request):
|
| 381 |
-
seen.append((request.method, request.url.path, request.headers.get("cookie")))
|
| 382 |
-
if request.url.path == "/guess":
|
| 383 |
-
return httpx.Response(200, json={"output": "ok"}, headers={"set-cookie": "session=abc; path=/"})
|
| 384 |
-
return httpx.Response(200, json={"task_id": "t1", "prompt": "p", "answer": "SECRET", "meta": {"ground_truth": "TRUTHVAL"}})
|
| 385 |
-
|
| 386 |
-
real = live.SpaceClient
|
| 387 |
-
monkeypatch.setattr(live, "SpaceClient", lambda **kw: real(transport=httpx.MockTransport(handler), **kw))
|
| 388 |
-
sid = live.start("org/env", "web:a")["session"]
|
| 389 |
-
out = live.act(sid, "web:a", "http", {"method": "POST", "path": "/guess", "body": {"word": "crane"}})["result"]
|
| 390 |
-
assert out["status"] == 200 and out["json"] == {"output": "ok"}
|
| 391 |
-
out = live.act(sid, "web:a", "http", {"method": "GET", "path": "/tasks/{task_id}", "params": {"task_id": "t1"}})["result"]
|
| 392 |
-
assert "SECRET" not in json.dumps(out) and "TRUTHVAL" not in json.dumps(out) and set(out["withheld"]) == {"answer", "ground_truth"}
|
| 393 |
-
assert seen[0][0] == "POST" and seen[1][2] == "session=abc" # the session's own cookie, kept server-side
|
| 394 |
-
with pytest.raises(live.SpaceError):
|
| 395 |
-
live.act(sid, "web:a", "http", {"method": "PUT", "path": "/guess"})
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
def test_a_reset_step_server_still_lists_its_tools(monkeypatch):
|
| 399 |
-
"""No MCP tools of its own (OpenEnv answers "Environment does not support MCP"): the page gets exactly what the MCP
|
| 400 |
-
bridge offers an agent, step taking the server's own action fields. Tool servers use the same descriptor."""
|
| 401 |
-
info = {"running": True, "stage": "RUNNING", "step_api": True, "mcp": None, "task_api": None,
|
| 402 |
-
"schema": {"action": {"type": "object", "properties": {"latex": {"type": "string", "description": "Predicted LaTeX"}}, "required": ["latex"]}}}
|
| 403 |
-
monkeypatch.setattr(live, "probe", lambda spec, fresh=False: info)
|
| 404 |
-
tools = {t["name"]: t for t in client.get("/api/spaces/o/latex/live").json()["bridge_tools"]}
|
| 405 |
-
assert set(tools) == {"reset", "step", "state"}
|
| 406 |
-
assert "latex" in json.dumps(tools["step"]["inputSchema"])
|
| 407 |
-
assert tools == {t["name"]: t for t in mcp_bridge._tools(info)} # the page and the bridge never disagree
|
| 408 |
-
monkeypatch.setattr(live, "probe", lambda spec, fresh=False: {**info, "mcp": [{"name": "look", "inputSchema": {"type": "object"}}]})
|
| 409 |
-
assert {t["name"] for t in client.get("/api/spaces/o/geo/live").json()["bridge_tools"]} == {"look", "reset", "state"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_mimo.py
DELETED
|
@@ -1,187 +0,0 @@
|
|
| 1 |
-
"""The MiMo release behind the environment contract (app/envs/mimo.py, app/mimo): its tasks read the way the MiMo RL
|
| 2 |
-
Environment Explorer reads them, run as their Harbor conversion or on the release's own harness, and linked to their
|
| 3 |
-
Harbor twins both ways (no network: the release's rows and task views are stubbed; its browsing index is the real one).
|
| 4 |
-
|
| 5 |
-
What is checked: the MiMo adapter claims the release and nothing else; tiles, scoped facets and the map are coherent;
|
| 6 |
-
every task is a card; a raw row's ref opens the same task; the Harbor twin is the default unless its grader can't score
|
| 7 |
-
here; a rubric task's judge is required and must come from the offered pool; the MiMo runner gets the task and the
|
| 8 |
-
explorer's fields, never a token in its record; a twin links back to the release; a MiMo rollout's page serves its
|
| 9 |
-
events, scrubbed for the public; custom data refuses paths outside the workspace, and raw files are sandboxed.
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
from __future__ import annotations
|
| 13 |
-
|
| 14 |
-
import json
|
| 15 |
-
import time
|
| 16 |
-
|
| 17 |
-
import pytest
|
| 18 |
-
from fastapi.testclient import TestClient
|
| 19 |
-
|
| 20 |
-
from app import auth, main, models, settings, store
|
| 21 |
-
from app.envs import contract as c, mimo, registry
|
| 22 |
-
from app.mimo import catalog as mcat
|
| 23 |
-
|
| 24 |
-
client = TestClient(main.app, base_url="https://testserver")
|
| 25 |
-
TOKEN = "hf_mimo_secret_token_0004"
|
| 26 |
-
SPEC = mimo.SPEC
|
| 27 |
-
GENERAL = "s3k_0052_accounting_audit_tax_en_t2_rl_008"
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def cookie() -> dict[str, str]:
|
| 31 |
-
session = {"token": TOKEN, "name": "erin", "avatar": None, "via": "token", "exp": time.time() + 3600}
|
| 32 |
-
return {"Cookie": f"{auth.COOKIE}={auth._box.encrypt(json.dumps(session).encode()).decode()}"}
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def view(tid: str) -> dict:
|
| 36 |
-
rec = mcat.record(tid)
|
| 37 |
-
d = rec["d"]
|
| 38 |
-
v = {"id": tid, "domain": d, "title": rec["t"], "short_title": rec["t"], "facets": rec["f"], "brief": "Do the thing.", "meta": [],
|
| 39 |
-
"image": "docker.io/x:y", "runnable": True, "environment": {"cwd": "/work", "image": "docker.io/x:y"}, "prompt": {"parts": [["task", "Do the thing."]], "task_is_brief": True}}
|
| 40 |
-
if d == "general":
|
| 41 |
-
v["verify"] = {"kind": "rubric", "summary": "Checked.", "needs_judge": "text", "formula": "Σ",
|
| 42 |
-
"checks": [{"id": "c1", "tier": "critical", "method": "llm", "weight": 2, "question": "Is it right?", "files": []}]}
|
| 43 |
-
else:
|
| 44 |
-
v["verify"] = {"kind": "tests", "summary": "Tests.", "files": [{"path": "t.py", "added": 1, "removed": 0, "new": True}], "formula": "1 or 0"}
|
| 45 |
-
return v
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
@pytest.fixture(autouse=True)
|
| 49 |
-
def stubs(monkeypatch):
|
| 50 |
-
ids = [e["id"] for e in mcat.index()["envs"]]
|
| 51 |
-
by_domain: dict[str, list[str]] = {}
|
| 52 |
-
for e in mcat.index()["envs"]:
|
| 53 |
-
by_domain.setdefault(e["d"], []).append(e["id"])
|
| 54 |
-
monkeypatch.setattr(mcat, "rows", lambda: {tid: {"domain": d} for d, tids in by_domain.items() for tid in tids})
|
| 55 |
-
monkeypatch.setattr(mcat, "view", lambda tid: view(tid) if tid in set(ids) else None)
|
| 56 |
-
mimo._order.cache_clear(); mimo._position.cache_clear()
|
| 57 |
-
monkeypatch.setattr(registry, "_meta", lambda kind, spec, token: {"id": spec, "sha": "abc", "restricted": False})
|
| 58 |
-
monkeypatch.setattr(registry, "_choice", {})
|
| 59 |
-
monkeypatch.setattr("app.mimo.runner.domains.run_defaults", lambda tid, d: {"steps": 100, "timeout_min": 30, "max_tokens": None})
|
| 60 |
-
monkeypatch.setattr(models, "get", lambda mid: {"id": mid, "tools": True, "provider": "x"})
|
| 61 |
-
monkeypatch.setattr(models, "catalog", lambda: {"text_judges": [{"id": "judge/text"}], "vision_judges": [{"id": "judge/vision"}], "agents": []})
|
| 62 |
-
monkeypatch.setattr(settings, "_org_members", lambda: set())
|
| 63 |
-
yield
|
| 64 |
-
mimo._order.cache_clear(); mimo._position.cache_clear()
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
def twin_opt(monkeypatch, warnings=()):
|
| 68 |
-
"""A Harbor twin for every task, whose grader may need a key it won't get here."""
|
| 69 |
-
from app.envs import harbor
|
| 70 |
-
|
| 71 |
-
monkeypatch.setattr(mimo.MiMoAdapter, "_twin", lambda self, tid, d: {"dataset": f"FineEnvs/MiMo-V2.6-RL-harbor-{d}", "path": f"tasks/{tid}"})
|
| 72 |
-
toml = "[verifier.env]\n" + "".join(f'{k} = "${{{k}}}"\n' for k in warnings)
|
| 73 |
-
monkeypatch.setattr("app.catalog.task", lambda d, p, t=None: {"runnable": {"ok": True, "how": "image"}, "verifier": {"env": list(warnings)},
|
| 74 |
-
"env": {"image": "img"}, "bytes": 1, "toml": toml})
|
| 75 |
-
return harbor
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
def test_the_release_is_read_by_the_mimo_adapter_only():
|
| 79 |
-
a = mimo.MiMoAdapter()
|
| 80 |
-
assert a.detect("dataset", {"id": SPEC}) == 1.0 and a.detect("dataset", {"id": "someone/else"}) == 0
|
| 81 |
-
assert registry.resolve("dataset", SPEC).adapter.id == "mimo"
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
def test_tiles_facets_and_map_are_coherent():
|
| 85 |
-
s = client.get(f"/api/env/{SPEC}").json()
|
| 86 |
-
keys = [f["key"] for f in s["facets"]]
|
| 87 |
-
assert len(keys) == len(set(keys)), "one facet per key"
|
| 88 |
-
tiles = {t["value"] for t in s["tiles"]["items"]}
|
| 89 |
-
assert tiles == {"Code", "Webdev", "Cyber", "Music", "General"} and s["tiles"]["key"] == "domain"
|
| 90 |
-
for f in s["facets"]:
|
| 91 |
-
assert set(f["scope"]) <= tiles | {"*"}
|
| 92 |
-
assert set(s["map"]["by_tile"]) == tiles and all(k in keys for k in s["map"]["by_tile"].values())
|
| 93 |
-
assert s["order"] == "shuffle" and s["overview"][0]["collapsed"] is True
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
def test_every_task_is_a_card():
|
| 97 |
-
d = client.get(f"/api/env/{SPEC}/tasks?all=1").json()
|
| 98 |
-
assert d["total"] == len(mcat.index()["envs"]) == len(d["cards"])
|
| 99 |
-
assert all(x["facets"]["domain"] and x["color"] for x in d["cards"])
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
def test_a_raw_rows_ref_opens_the_same_task(monkeypatch):
|
| 103 |
-
twin_opt(monkeypatch)
|
| 104 |
-
first = mimo._order()["code"][6]
|
| 105 |
-
v = client.get(f"/api/env/{SPEC}/task?ref=code/train/6").json()
|
| 106 |
-
assert v["ref"] == first and v["nav"]["index"] == 6
|
| 107 |
-
assert client.get(f"/api/env/{SPEC}/task?ref=code/train/999999").status_code == 404
|
| 108 |
-
assert client.get(f"/api/env/{SPEC}/task?ref=nope").status_code == 404
|
| 109 |
-
# by its own ref, its row is an alias (rollouts recorded against the row are listed with it)
|
| 110 |
-
assert {"env": SPEC, "ref": "code/train/6"} in client.get(f"/api/env/{SPEC}/task?ref={first}").json()["aliases"]
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def test_harbor_is_the_default_and_a_model_graded_twin_gets_a_judge(monkeypatch):
|
| 114 |
-
twin_opt(monkeypatch)
|
| 115 |
-
code = mimo._order()["code"][0]
|
| 116 |
-
opts = client.get(f"/api/env/{SPEC}/task?ref={code}").json()["run"]["options"]
|
| 117 |
-
assert [(o["runner"], o["default"]) for o in opts] == [("harbor", True), ("mimo", False)]
|
| 118 |
-
assert not any(f["key"] == "judge" for f in opts[0]["fields"])
|
| 119 |
-
twin_opt(monkeypatch, warnings=["GA_JUDGE_KEY"]) # its grader calls a model: Harbor still runs it, through the relay
|
| 120 |
-
opts = client.get(f"/api/env/{SPEC}/task?ref={GENERAL}").json()["run"]["options"]
|
| 121 |
-
assert [(o["runner"], o["default"]) for o in opts] == [("harbor", True), ("mimo", False)]
|
| 122 |
-
for o in opts:
|
| 123 |
-
judge = next(f for f in o["fields"] if f["key"] == "judge")
|
| 124 |
-
assert judge["type"] == "model" and judge["pool"] == "text_judges" and judge["required"]
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
def test_mimo_runs_get_the_task_and_checked_fields(monkeypatch):
|
| 128 |
-
twin_opt(monkeypatch)
|
| 129 |
-
from app.mimo.runner import core
|
| 130 |
-
|
| 131 |
-
seen = {}
|
| 132 |
-
|
| 133 |
-
def fake_submit(user, token, task, model, provider, judge, **kw):
|
| 134 |
-
seen.update(user=user, token=token, task=task, judge=judge, **kw)
|
| 135 |
-
return store.create({"id": "20260103-000000-mimo01", "user": user, "task_id": task["id"], "status": "queued", **kw["extra"]})
|
| 136 |
-
|
| 137 |
-
monkeypatch.setattr(core, "submit", fake_submit)
|
| 138 |
-
post = lambda **kw: client.post("/api/runs", json={"dataset": SPEC, "path": GENERAL, "runner": "mimo", "model": "m/x", **kw}, headers=cookie())
|
| 139 |
-
assert post().status_code == 400 # a rubric task needs a judge
|
| 140 |
-
assert post(fields={"judge": "not/offered"}).status_code == 400 # from the pool only
|
| 141 |
-
assert post(fields={"judge": "judge/text", "thinking": "loud"}).status_code == 400
|
| 142 |
-
r = post(fields={"judge": "judge/text", "thinking": "high", "temperature": 0.5})
|
| 143 |
-
assert r.status_code == 200, r.text
|
| 144 |
-
assert seen["task"]["id"] == GENERAL and seen["judge"] == "judge/text" and seen["params"] == {"thinking": "high", "temperature": 0.5}
|
| 145 |
-
assert seen["extra"] == {"dataset": SPEC, "path": GENERAL, "env": SPEC, "adapter": "mimo", "runner": "mimo", "restricted": False, "sha": "abc"}
|
| 146 |
-
assert TOKEN not in json.dumps(store.get("20260103-000000-mimo01"))
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
def test_a_harbor_twin_links_back_to_the_release():
|
| 150 |
-
tid = mimo._order()["code"][3]
|
| 151 |
-
from app.envs.processors import _slug
|
| 152 |
-
|
| 153 |
-
env = c.Env("dataset", "FineEnvs/MiMo-V2.6-RL-harbor-code", {}, None)
|
| 154 |
-
links = mimo.harbor_to_mimo(env, f"tasks/{_slug(tid)}", {})
|
| 155 |
-
assert links and links[0]["rel"] == "same" and links[0]["href"] == f"/t/{SPEC}/{tid}"
|
| 156 |
-
assert (SPEC, tid) in mimo.harbor_aliases(env, f"tasks/{_slug(tid)}")
|
| 157 |
-
assert mimo.harbor_to_mimo(c.Env("dataset", "someone/else", {}, None), f"tasks/{_slug(tid)}", {}) == []
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
def test_a_mimo_rollouts_page_serves_its_events_scrubbed_for_the_public():
|
| 161 |
-
rid = "20260103-000000-mimo02"
|
| 162 |
-
store.create({"id": rid, "user": "erin", "task_id": GENERAL, "dataset": SPEC, "path": GENERAL, "runner": "mimo", "status": "done", "reward": 1.0,
|
| 163 |
-
"visibility": "public", "title": "T", "model": "m/x", "endpoint": {"base_url": "https://erin.example/v1", "host": "erin.example"}})
|
| 164 |
-
store.append_events(rid, [{"i": 0, "t": 0, "kind": "text", "text": "hello from erin at https://erin.example/v1"}])
|
| 165 |
-
mine = client.get(f"/api/runs/{rid}", headers=cookie()).json()
|
| 166 |
-
assert mine["events"][0]["text"].startswith("hello from erin") and mine["run"]["is_owner"]
|
| 167 |
-
pub = client.get(f"/api/runs/{rid}").json()
|
| 168 |
-
assert "erin" not in json.dumps(pub["events"]) and "erin" not in json.dumps(pub["run"])
|
| 169 |
-
assert client.get(f"/api/runs/{rid}?after=1").json()["events"] == []
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
def test_custom_data_stays_inside_the_workspace(monkeypatch):
|
| 173 |
-
def outside(tid, rel):
|
| 174 |
-
raise PermissionError(rel)
|
| 175 |
-
|
| 176 |
-
monkeypatch.setattr(mcat, "workspace_path", outside)
|
| 177 |
-
assert client.get(f"/api/env/{SPEC}/data?ref={GENERAL}&part=preview&path=../../etc/passwd").status_code == 404
|
| 178 |
-
assert client.get(f"/api/env/{SPEC}/raw?ref={GENERAL}&f=../../etc/passwd").status_code == 404
|
| 179 |
-
assert client.get(f"/api/env/{SPEC}/data?ref={GENERAL}&part=nothing").status_code == 404
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
def test_raw_files_are_sandboxed(monkeypatch, tmp_path):
|
| 183 |
-
page = tmp_path / "index.html"
|
| 184 |
-
page.write_text("<script>alert(1)</script>")
|
| 185 |
-
monkeypatch.setattr(mcat, "workspace_path", lambda tid, rel: page)
|
| 186 |
-
r = client.get(f"/api/env/{SPEC}/raw?ref={GENERAL}&f=index.html")
|
| 187 |
-
assert r.status_code == 200 and r.headers["content-security-policy"] == "sandbox" and r.headers["x-content-type-options"] == "nosniff"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_model_relay.py
DELETED
|
@@ -1,23 +0,0 @@
|
|
| 1 |
-
from fastapi.testclient import TestClient
|
| 2 |
-
|
| 3 |
-
from app import main, runner, runtime
|
| 4 |
-
from app.mimo.runner import core
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
def test_local_relay_precedes_capture_catchall_and_keeps_capability_auth(monkeypatch):
|
| 8 |
-
monkeypatch.setattr(runner, "_service", None)
|
| 9 |
-
client = TestClient(runner.service().capture.app)
|
| 10 |
-
monkeypatch.setattr(core, "by_cap", lambda cap: None)
|
| 11 |
-
assert client.get("/rlx/llm/invalid/health").status_code == 404
|
| 12 |
-
assert client.post("/rlx/llm/invalid/v1/chat/completions", json={}).status_code == 404
|
| 13 |
-
assert client.post("/v1/chat/completions", json={"model": "test", "messages": []}).status_code == 401
|
| 14 |
-
async def relay(cap, request):
|
| 15 |
-
return {"route": "scoped-relay"}
|
| 16 |
-
monkeypatch.setattr(main, "llm_proxy", relay)
|
| 17 |
-
assert client.post("/rlx/llm/test/v1/chat/completions", json={}).json() == {"route": "scoped-relay"}
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
def test_local_and_deployed_capabilities_are_redacted_from_logs():
|
| 21 |
-
for prefix in ("api", "rlx"):
|
| 22 |
-
msg = f"POST /{prefix}/llm/SECRET-CAP/v1/chat/completions"
|
| 23 |
-
assert "SECRET-CAP" not in runtime.SECRET_PATH.sub(lambda m: (m.group(1) or m.group(2)) + "[hidden]", msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_nemo_runner.py
DELETED
|
@@ -1,73 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
|
| 3 |
-
import pytest
|
| 4 |
-
|
| 5 |
-
from app import nemo_runner as runner
|
| 6 |
-
from app.envs import nemogym
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
def row():
|
| 10 |
-
return {"responses_create_params": {"input": [{"role": "user", "content": "Look up A."},
|
| 11 |
-
{"type": "reasoning", "summary": [{"type": "summary_text", "text": "Previous reasoning"}]}],
|
| 12 |
-
"tools": [{"type": "function", "name": "lookup", "parameters": {"type": "object"}}]},
|
| 13 |
-
"expected_action": {"type": "function_call", "name": "lookup", "arguments": '{"id":"PRIVATE-ANSWER"}'}}
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def test_responses_input_preserved_and_answer_withheld():
|
| 17 |
-
data = row()
|
| 18 |
-
out = runner.responses_request(data)
|
| 19 |
-
assert out["input"] == data["responses_create_params"]["input"]
|
| 20 |
-
assert "PRIVATE-ANSWER" not in json.dumps(out)
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def test_provider_hosted_tools_cannot_execute():
|
| 24 |
-
data = row()
|
| 25 |
-
data["responses_create_params"]["tools"] = [{"type": "mcp", "server_url": "https://example.com", "require_approval": "never"}]
|
| 26 |
-
with pytest.raises(ValueError, match="Only function"):
|
| 27 |
-
runner.responses_request(data)
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
def test_native_comparator_scores_arguments_and_calls_before_text():
|
| 31 |
-
data = row()
|
| 32 |
-
agent = nemogym.agent_of({}, "nvidia/Nemotron-RL-Agentic-Function-Calling-Pivot-v1")
|
| 33 |
-
expected = runner.validate(data, agent)
|
| 34 |
-
response = {"output": [{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Looking it up"}]},
|
| 35 |
-
{"type": "function_call", "name": "lookup", "arguments": '{"id":"PRIVATE-ANSWER"}'}]}
|
| 36 |
-
assert runner.score(expected, runner.prediction(response))[0] == 1
|
| 37 |
-
response["output"][1]["arguments"] = '{"id":"wrong"}'
|
| 38 |
-
assert runner.score(expected, runner.prediction(response))[0] == 0
|
| 39 |
-
response["output"][1]["arguments"] = 'not json'
|
| 40 |
-
assert runner.score(expected, runner.prediction(response))[0] == 0
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def test_invalid_reference_does_not_leak_through_errors():
|
| 44 |
-
data = row()
|
| 45 |
-
data["expected_action"] = {"type": "PRIVATE-ANSWER"}
|
| 46 |
-
with pytest.raises(ValueError) as e:
|
| 47 |
-
runner.validate(data, nemogym.agent_of({}, "nvidia/Nemotron-RL-Agentic-Function-Calling-Pivot-v1"))
|
| 48 |
-
assert "PRIVATE-ANSWER" not in str(e.value)
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
def test_unknown_nemo_server_stays_native():
|
| 52 |
-
with pytest.raises(ValueError, match="not hosted"):
|
| 53 |
-
runner.validate(row(), {"known": True, "resources_server": "another_server"})
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def test_provider_failure_inside_http_200_is_not_a_zero_reward():
|
| 57 |
-
with pytest.raises(ValueError, match="did not produce a prediction"):
|
| 58 |
-
runner.prediction({"status": "failed", "error": {"message": "invalid schema"}, "output": []})
|
| 59 |
-
with pytest.raises(ValueError, match="invalid Responses"):
|
| 60 |
-
runner.prediction({"message": "not a response"})
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def test_invalid_strict_declaration_is_relaxed_without_changing_parameters():
|
| 64 |
-
data = row()
|
| 65 |
-
tool = data["responses_create_params"]["tools"][0]
|
| 66 |
-
tool["strict"] = True
|
| 67 |
-
original = json.dumps(data)
|
| 68 |
-
request = runner.responses_request(data)
|
| 69 |
-
assert request["tools"][0]["strict"] is False
|
| 70 |
-
assert request["tools"][0]["parameters"] == tool["parameters"]
|
| 71 |
-
assert json.dumps(data) == original
|
| 72 |
-
tool["parameters"].update(properties={}, required=[], additionalProperties=False)
|
| 73 |
-
assert runner.responses_request(data)["tools"][0]["strict"] is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_remote_mcp.py
DELETED
|
@@ -1,62 +0,0 @@
|
|
| 1 |
-
"""Standard MCP sessions preserve cookies, negotiated headers and reply identity."""
|
| 2 |
-
import json
|
| 3 |
-
|
| 4 |
-
import httpx
|
| 5 |
-
import pytest
|
| 6 |
-
|
| 7 |
-
from app import spaces_live as live
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def test_standard_mcp_initializes_isolates_and_closes_sessions():
|
| 11 |
-
seen, next_id = [], []
|
| 12 |
-
def handle(request):
|
| 13 |
-
seen.append(request)
|
| 14 |
-
if request.method == "DELETE":
|
| 15 |
-
return httpx.Response(204)
|
| 16 |
-
message = json.loads(request.content)
|
| 17 |
-
if message["method"] == "initialize":
|
| 18 |
-
sid = str(len(next_id) + 1)
|
| 19 |
-
next_id.append(sid)
|
| 20 |
-
return httpx.Response(200, headers={"Mcp-Session-Id": sid, "Set-Cookie": f"episode={sid}; Path=/"},
|
| 21 |
-
json={"jsonrpc": "2.0", "id": message["id"], "result": {"protocolVersion": "2025-06-18"}})
|
| 22 |
-
sid = request.headers["mcp-session-id"]
|
| 23 |
-
assert request.headers["mcp-protocol-version"] == "2025-06-18"
|
| 24 |
-
assert request.headers["cookie"] == f"episode={sid}"
|
| 25 |
-
if message["method"] == "notifications/initialized":
|
| 26 |
-
assert "id" not in message
|
| 27 |
-
return httpx.Response(202)
|
| 28 |
-
# A progress notification is not the result; the actual event has multi-line data.
|
| 29 |
-
event = json.dumps({"jsonrpc": "2.0", "id": message["id"], "result": {"episode": sid}}, indent=2)
|
| 30 |
-
text = 'data: {"jsonrpc":"2.0","method":"notifications/progress"}\n\n' + '\n'.join('data: '+line for line in event.splitlines())+'\n\n'
|
| 31 |
-
return httpx.Response(200, headers={"Content-Type": "text/event-stream"}, text=text)
|
| 32 |
-
with live.SpaceClient(transport=httpx.MockTransport(handle)) as a, live.SpaceClient(transport=httpx.MockTransport(handle)) as b:
|
| 33 |
-
first = live.RemoteMCP({"host": "https://fixture.hf.space"}, a)
|
| 34 |
-
second = live.RemoteMCP({"host": "https://fixture.hf.space"}, b)
|
| 35 |
-
assert first.call("tools/call", {}, 2) == {"episode": "1"}
|
| 36 |
-
assert second.call("tools/call", {}, 2) == {"episode": "2"}
|
| 37 |
-
assert first.call("tools/call", {}, 2) == {"episode": "1"}
|
| 38 |
-
first.close()
|
| 39 |
-
second.close()
|
| 40 |
-
assert len(next_id) == 2
|
| 41 |
-
assert [r.headers["mcp-session-id"] for r in seen if r.method == "DELETE"] == ["1", "2"]
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
def test_expired_mcp_session_does_not_replay_or_reinitialize():
|
| 45 |
-
calls = []
|
| 46 |
-
def handle(request):
|
| 47 |
-
calls.append(request)
|
| 48 |
-
return httpx.Response(404)
|
| 49 |
-
with live.SpaceClient(transport=httpx.MockTransport(handle)) as c:
|
| 50 |
-
remote = live.RemoteMCP({"host": "https://fixture.hf.space"}, c)
|
| 51 |
-
remote.initialized, remote.session = True, "existing"
|
| 52 |
-
for _ in range(2):
|
| 53 |
-
with pytest.raises(live.SpaceError) as error:
|
| 54 |
-
remote.call("tools/call", {"name": "step"}, 2)
|
| 55 |
-
assert error.value.status == 410
|
| 56 |
-
assert len(calls) == 1
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def test_mcp_rejects_an_unrelated_rpc_response():
|
| 60 |
-
with live.SpaceClient(transport=httpx.MockTransport(lambda r: httpx.Response(200, json={"jsonrpc": "2.0", "id": "other", "result": {}}))) as c:
|
| 61 |
-
with pytest.raises(live.SpaceError, match="matching"):
|
| 62 |
-
live.RemoteMCP({"host": "https://fixture.hf.space"}, c).call("tools/list", {}, 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_search_api.py
DELETED
|
@@ -1,629 +0,0 @@
|
|
| 1 |
-
"""/api/search and /api/search/tasks (app/search_api.py) over a catalog snapshot (no network: the listing is fixture rows).
|
| 2 |
-
|
| 3 |
-
What is checked: declared framework taxonomy independent of editorial collections; metric-only trending with
|
| 4 |
-
separate featured picks; hidden environments left out of everything at query time; every answer (order, total, every
|
| 5 |
-
facet's counts) equal to a plain-Python port of the page's own filtering over the same rows, for hundreds of queries
|
| 6 |
-
(a parity test), through a published snapshot and through the live fallback alike; search text never read as FTS
|
| 7 |
-
syntax; the visitor's own private datasets merged in; cache headers and ETags.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import gzip
|
| 13 |
-
import json
|
| 14 |
-
import random
|
| 15 |
-
import shutil
|
| 16 |
-
import time
|
| 17 |
-
from pathlib import Path
|
| 18 |
-
|
| 19 |
-
import pytest
|
| 20 |
-
from fastapi import FastAPI
|
| 21 |
-
from fastapi.testclient import TestClient
|
| 22 |
-
|
| 23 |
-
from app import catalog, config, indexer, search_api, snapshot
|
| 24 |
-
|
| 25 |
-
app = FastAPI()
|
| 26 |
-
app.include_router(search_api.router)
|
| 27 |
-
client = TestClient(app)
|
| 28 |
-
|
| 29 |
-
TAGS = ["code", "agents", "math", "terminal", "swe", "sql", "vision", "a\"quote", "c++", "日本語", "NEAR", "tool-use"]
|
| 30 |
-
COLLS = [{"id": "fineenvs", "group": "FineEnvs on OpenEnv", "icon": "globe", "color": "amber", "about": "ours",
|
| 31 |
-
"ids": ["space:fe/geo", "space:fe/latex", "space:fe/ors-one", "space:fe/plain"]},
|
| 32 |
-
{"id": "terminal", "group": "Terminal-Bench", "icon": "terminal", "color": "cyber", "about": "terminal tasks",
|
| 33 |
-
"ids": ["tb/bench-1", "tb/bench-2", "space:fe/plain"]}] # fe/plain is in both: the first one counts
|
| 34 |
-
PINS = ["space:fe/latex", "ds/d07"]
|
| 35 |
-
HIDDEN = {"ds/d13", "space:sp/s05"}
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
def rows_fixture(n_ds: int = 30, n_sp: int = 40, seed: int = 7) -> list[dict]:
|
| 39 |
-
"""Listing rows as catalog.environments() returns them (datasets' framework already resolved), varied enough to
|
| 40 |
-
exercise every facet, sort and tie."""
|
| 41 |
-
rnd = random.Random(seed)
|
| 42 |
-
fws = ["harbor", "harbor", "harbor", "verifiers", "nemo-gym", "openenv-data", "verl", "rows", None]
|
| 43 |
-
out = []
|
| 44 |
-
for i in range(n_ds):
|
| 45 |
-
fw = fws[i % len(fws)]
|
| 46 |
-
tasks = rnd.choice([None, None, 0, 5, 99, 100, 999, 1000, 4000]) if fw in ("harbor", None) else None
|
| 47 |
-
out.append({"id": f"ds/d{i:02d}", "key": f"ds/d{i:02d}", "kind": "dataset", "framework": fw,
|
| 48 |
-
"heading": rnd.choice([None, f"Bench {i}", "Terminal tasks", "Data analysis"]),
|
| 49 |
-
"brief": rnd.choice(["", "Agents solve SQL tasks in a sandbox.", "Hard math, graded by tests.", "Tools and C++ builds."]),
|
| 50 |
-
"downloads": rnd.choice([0, 5, 50, 9999, 20000, 5]), "likes": rnd.choice([0, 1, 1, 3, 40]),
|
| 51 |
-
"trending": rnd.choice([0, 0, 1, 2, 5]), "updated": f"2026-0{rnd.randint(1, 9)}-1{rnd.randint(0, 9)}T10:00:00+00:00",
|
| 52 |
-
"created": rnd.choice([None, "2025-01-02T00:00:00.123+00:00", f"2026-0{rnd.randint(1, 9)}-01T00:00:00Z"]),
|
| 53 |
-
"badges": rnd.choice([[], ["RL Environment"], ["Benchmark"], ["RL Environment", "Benchmark"]]),
|
| 54 |
-
"tags": rnd.sample(TAGS, rnd.randint(0, 4)), "private": False, "gated": False,
|
| 55 |
-
"indexed": {"tasks": tasks, "graded": {"txt": 3}, "image": 1} if tasks is not None else None})
|
| 56 |
-
out += [{"id": "tb/bench-1", "key": "tb/bench-1", "kind": "dataset", "framework": "harbor", "heading": "TB one", "brief": "Terminal",
|
| 57 |
-
"downloads": 100, "likes": 2, "trending": 3, "badges": ["Benchmark"], "tags": ["terminal"], "indexed": {"tasks": 89},
|
| 58 |
-
"updated": "2026-10-01T00:00:00+00:00"},
|
| 59 |
-
{"id": "tb/bench-2", "key": "tb/bench-2", "kind": "dataset", "framework": "verifiers", "heading": None, "brief": "x" * 40,
|
| 60 |
-
"downloads": 100, "likes": 2, "trending": 3, "badges": [], "tags": [], "indexed": None}]
|
| 61 |
-
for i in range(n_sp):
|
| 62 |
-
fw = rnd.choice(["openenv", "openenv", "openenv", "ors", "space"])
|
| 63 |
-
out.append({"id": f"sp/s{i:02d}", "key": f"space:sp/s{i:02d}", "kind": "space", "framework": fw, "openenv": fw == "openenv",
|
| 64 |
-
"heading": rnd.choice([None, f"Game {i}", "Wordle"]), "brief": rnd.choice(["", "Play a game.", "An OCR env."]),
|
| 65 |
-
"downloads": 0, "likes": rnd.choice([0, 1, 2, 2, 10]), "trending": rnd.choice([0, 1, 1, 4]),
|
| 66 |
-
"updated": rnd.choice([None, f"2026-0{rnd.randint(1, 9)}-0{rnd.randint(1, 9)}T00:00:00+00:00"]),
|
| 67 |
-
"created": f"2025-1{rnd.randint(0, 2)}-01T00:00:00+00:00", "stage": rnd.choice(["RUNNING", "SLEEPING", None, "RUNTIME_ERROR"]),
|
| 68 |
-
"mcp": rnd.random() < 0.3, "openenv_version": rnd.choice([None, None, "0.2.1", "0.2.3"]) if fw == "openenv" else None,
|
| 69 |
-
"badges": ["OpenEnv"] if fw == "openenv" else ["ORS"] if fw == "ors" else [], "tags": rnd.sample(TAGS, rnd.randint(0, 3)),
|
| 70 |
-
"private": False, "gated": False, "hardware": "cpu-basic", "manifest": "openenv.yaml" if fw == "openenv" else None})
|
| 71 |
-
for sid, fw in (("fe/geo", "openenv"), ("fe/latex", "space"), ("fe/ors-one", "ors"), ("fe/plain", "space")):
|
| 72 |
-
out.append({"id": sid, "key": f"space:{sid}", "kind": "space", "framework": fw, "openenv": fw == "openenv", "heading": sid,
|
| 73 |
-
"brief": "FineEnvs server", "downloads": 0, "likes": 1, "trending": 0, "stage": "RUNNING", "mcp": True,
|
| 74 |
-
"badges": ["OpenEnv"] if fw == "openenv" else ["ORS"] if fw == "ors" else [], "tags": ["agents"]})
|
| 75 |
-
out.append({"id": "priv/secret", "key": "priv/secret", "kind": "dataset", "framework": "harbor", "private": True, "heading": "Secret",
|
| 76 |
-
"brief": "", "tags": [], "badges": [], "downloads": 0, "likes": 0, "trending": 99})
|
| 77 |
-
return out
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
ROWS = rows_fixture()
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def write_index(spec: str, tasks: list[dict], restricted: bool = False) -> None:
|
| 84 |
-
idx = {"spec": spec, "sha": "abc", "built": 1.0, "version": catalog.INDEX_VERSION, "info": {"restricted": restricted},
|
| 85 |
-
"tasks": tasks, "summary": {}}
|
| 86 |
-
catalog.atomic_write(catalog._index_path(spec), gzip.compress(json.dumps(idx).encode()))
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
TASKS = {"ds/d00": [{"path": "tasks/sql-join", "title": "Join two tables in SQL", "brief": "Write a query.", "category": "sql",
|
| 90 |
-
"difficulty": "easy", "tags": ["sql"], "verifier": {"kind": "txt"}, "env": {}, "run": "image"},
|
| 91 |
-
{"path": "tasks/foo-only", "title": "Foo the bar", "brief": "Nothing else.", "category": "misc", "tags": [],
|
| 92 |
-
"verifier": {}, "env": {}}],
|
| 93 |
-
"ds/d13": [{"path": "tasks/hidden-one", "title": "Join hidden tables", "brief": "", "tags": [], "verifier": {}, "env": {}}],
|
| 94 |
-
"priv/secret": [{"path": "tasks/s", "title": "Join secret tables", "brief": "", "tags": [], "verifier": {}, "env": {}}]}
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
@pytest.fixture(autouse=True)
|
| 98 |
-
def stubs(monkeypatch, tmp_path):
|
| 99 |
-
monkeypatch.setattr(catalog, "collections", lambda: COLLS)
|
| 100 |
-
monkeypatch.setattr(catalog, "pinned", lambda: list(PINS))
|
| 101 |
-
monkeypatch.setattr(catalog, "hidden", lambda: set(HIDDEN))
|
| 102 |
-
monkeypatch.setattr(catalog, "environments", lambda include_hidden=False: [r for r in ROWS if include_hidden or r["key"] not in HIDDEN])
|
| 103 |
-
monkeypatch.setattr(search_api, "rollout_counts", lambda: {"ds/d03": 4, "ds/d05": 1, "sp/s01": 2})
|
| 104 |
-
monkeypatch.setenv("RLX_SNAPSHOT_LOCAL", str(tmp_path / "local"))
|
| 105 |
-
monkeypatch.setenv("RLX_SNAPSHOT_STORE", str(tmp_path / "store"))
|
| 106 |
-
for spec, tasks in TASKS.items():
|
| 107 |
-
write_index(spec, tasks)
|
| 108 |
-
snapshot.reset()
|
| 109 |
-
search_api._answers.clear()
|
| 110 |
-
yield
|
| 111 |
-
snapshot.reset()
|
| 112 |
-
for spec in TASKS:
|
| 113 |
-
catalog._index_path(spec).unlink(missing_ok=True)
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
def publish_fixture(tmp_path: Path) -> dict:
|
| 117 |
-
"""ROWS as a published snapshot in the test store (as the indexer would: raw rows, admin settings left out)."""
|
| 118 |
-
store = snapshot.LocalStore(tmp_path / "store")
|
| 119 |
-
db = tmp_path / "build.db"
|
| 120 |
-
public = [r["id"] for r in ROWS if r["kind"] == "dataset"]
|
| 121 |
-
counts = snapshot.build_db(db, ROWS, snapshot.iter_index_tasks(public, mimo=False))
|
| 122 |
-
return indexer.publish(store, db, counts)
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def fw_js(d: dict) -> str:
|
| 126 |
-
"""No runtime evidence in the base fixture: metadata only supplies candidates."""
|
| 127 |
-
if d["kind"] == "space":
|
| 128 |
-
return "unverified-space" if d.get("framework") == "openenv" or d.get("openenv") else "other-space"
|
| 129 |
-
return d.get("framework") or "harbor"
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
def visible() -> list[dict]:
|
| 133 |
-
return [r for r in ROWS if r["key"] not in HIDDEN and not r.get("private")]
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
# ── the taxonomy and the orders ──────────────────────────────────────────────
|
| 137 |
-
@pytest.mark.parametrize("mode", ["snapshot", "live"])
|
| 138 |
-
def test_kind_taxonomy(tmp_path, mode):
|
| 139 |
-
if mode == "snapshot":
|
| 140 |
-
publish_fixture(tmp_path)
|
| 141 |
-
d = client.get("/api/search?size=100").json()
|
| 142 |
-
assert d["snapshot"]["source"] == mode
|
| 143 |
-
expected: dict[str, int] = {}
|
| 144 |
-
for r in visible():
|
| 145 |
-
k = fw_js(r)
|
| 146 |
-
expected[k] = expected.get(k, 0) + 1
|
| 147 |
-
assert {k: v for k, v in d["facets"]["kind"].items() if k != "all"} == expected
|
| 148 |
-
assert d["facets"]["kind"]["all"] == d["total"] == len(visible())
|
| 149 |
-
assert expected["other-space"] > 0 and expected["unverified-space"] > 0
|
| 150 |
-
cards = {c["key"]: c for c in d["rows"]}
|
| 151 |
-
# Collections cannot supply framework evidence, including for FineEnvs.
|
| 152 |
-
assert all(c["fw"] == "other-space" for c in cards.values() if c["kind"] == "space" and c["collection"] != "fineenvs" and c["framework"] == "ors")
|
| 153 |
-
for key in ("space:fe/latex", "space:fe/ors-one", "space:fe/plain"):
|
| 154 |
-
assert cards[key]["fw"] == "other-space" and not cards[key]["openenv"]
|
| 155 |
-
assert cards["space:fe/plain"]["collection"] == "fineenvs" # the first collection naming it
|
| 156 |
-
assert cards["ds/d08"]["fw"] == "harbor" # no framework at all: Harbor, as the page reads it
|
| 157 |
-
for k in expected:
|
| 158 |
-
sub = client.get(f"/api/search?kind={k}&size=100").json()
|
| 159 |
-
assert sub["total"] == expected[k] and all(c["fw"] == k for c in sub["rows"])
|
| 160 |
-
assert client.get("/api/search?k=ors").json()["query"]["kind"] == "other-space" # the page's old links
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
def test_trending_uses_metrics_and_featured_is_separate(tmp_path):
|
| 164 |
-
publish_fixture(tmp_path)
|
| 165 |
-
rows = client.get("/api/search?size=100").json()["rows"]
|
| 166 |
-
want = sorted(visible(), key=lambda r: (-r["trending"], -r["likes"], -r["downloads"]))
|
| 167 |
-
assert [r["key"] for r in rows] == [r["key"] for r in want]
|
| 168 |
-
oe = client.get("/api/search?kind=unverified-space&size=100").json()["rows"]
|
| 169 |
-
assert [r["key"] for r in oe] == [r["key"] for r in want if fw_js(r) == "unverified-space"]
|
| 170 |
-
assert client.get("/api/search?kind=unverified-space&sort=likes&size=100").json()["rows"][0]["likes"] == max(r["likes"] for r in oe)
|
| 171 |
-
trending = client.get("/api/search?trending=5&facets=0").json()["trending"]
|
| 172 |
-
assert [r["key"] for r in trending] == [r["key"] for r in rows[:5]]
|
| 173 |
-
assert [r["key"] for r in client.get("/api/search?trending=5").json()["featured"]] == PINS
|
| 174 |
-
filtered = client.get("/api/search?kind=unverified-space&trending=48").json()
|
| 175 |
-
assert filtered["featured"] == []
|
| 176 |
-
assert [r["key"] for r in filtered["trending"]] == [r["key"] for r in oe]
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
def test_hidden_environments_are_left_out_everywhere(tmp_path):
|
| 180 |
-
publish_fixture(tmp_path)
|
| 181 |
-
d = client.get("/api/search?size=100&trending=48").json()
|
| 182 |
-
keys = {r["key"] for r in d["rows"]} | {r["key"] for r in d["trending"]}
|
| 183 |
-
assert not keys & HIDDEN and "priv/secret" not in keys
|
| 184 |
-
assert d["stats"]["datasets"] + d["stats"]["spaces"] == len(visible())
|
| 185 |
-
assert client.get("/api/search?q=d13").json()["total"] == 0
|
| 186 |
-
assert client.get("/api/search/tasks?q=join").json()["rows"] == [
|
| 187 |
-
{"env": "ds/d00", "ref": "tasks/sql-join", "title": "Join two tables in SQL", "brief": "Write a query.", "category": "sql",
|
| 188 |
-
"difficulty": "easy", "grading": "txt", "run": "image"}] # not the hidden one's, nor the private one's
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
def test_an_admin_hiding_shows_without_a_new_snapshot(tmp_path, monkeypatch):
|
| 192 |
-
publish_fixture(tmp_path)
|
| 193 |
-
before = client.get("/api/search?q=d01").json()["total"]
|
| 194 |
-
assert client.get("/api/search?q=d01").json()["total"] == before == 1 # answered again (from memory)
|
| 195 |
-
monkeypatch.setattr(catalog, "hidden", lambda: set(HIDDEN) | {"ds/d01"})
|
| 196 |
-
assert client.get("/api/search?q=d01").json()["total"] == 0 # a settings change is a new question
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
def test_recent_openenv_checks_versions_and_trending_use_same_filters(tmp_path, monkeypatch):
|
| 200 |
-
from app import space_checks
|
| 201 |
-
import time
|
| 202 |
-
publish_fixture(tmp_path)
|
| 203 |
-
now = time.time()
|
| 204 |
-
evidence = {"fe/geo": {"schema": 1, "id": "fe/geo", "stage": "RUNNING", "checked_at": now,
|
| 205 |
-
"status": "API checked", "mode": "Simulation", "tools": 0,
|
| 206 |
-
"version": {"value": "0.2.3", "source": "Lockfile"}}}
|
| 207 |
-
monkeypatch.setattr(space_checks, "inventory", lambda: evidence)
|
| 208 |
-
params = {"kind": "openenv", "f": "health:API%20checked;oe:0.2.3", "trending": "24"}
|
| 209 |
-
data = client.get("/api/search", params=params).json()
|
| 210 |
-
assert data["total"] == 1
|
| 211 |
-
assert [r["id"] for r in data["rows"]] == [r["id"] for r in data["trending"]] == ["fe/geo"]
|
| 212 |
-
assert data["rows"][0]["version_source"] == "Lockfile"
|
| 213 |
-
assert data["rows"][0]["tools_status"] == "No tools discovered"
|
| 214 |
-
assert data["facets"]["oe"] == [["0.2.3", 1]]
|
| 215 |
-
assert client.get("/api/search", params={**params, "f": "health:API%20checked;tools:Tools%20discovered"}).json()["total"] == 0
|
| 216 |
-
# Negative checks and expiration invalidate cached search responses too.
|
| 217 |
-
evidence["fe/geo"]["status"] = "Checks failed"
|
| 218 |
-
assert client.get("/api/search", params=params).json()["total"] == 0
|
| 219 |
-
evidence["fe/geo"].update(status="API checked", checked_at=now - space_checks.FRESH - 1)
|
| 220 |
-
assert client.get("/api/search", params=params).json()["total"] == 0
|
| 221 |
-
expired = client.get("/api/search", params={"f": "health:Check%20expired"}).json()
|
| 222 |
-
assert expired["total"] == 1 and expired["rows"][0]["api_status"] == "Check expired"
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
def test_sql_python_parity_for_api_evidence(tmp_path, monkeypatch):
|
| 226 |
-
from app import space_checks
|
| 227 |
-
import time
|
| 228 |
-
publish_fixture(tmp_path)
|
| 229 |
-
now = time.time()
|
| 230 |
-
statuses = ["API checked", "Checks failed", "Not running", "Check unavailable"]
|
| 231 |
-
evidence = {r["id"]: {"id": r["id"], "status": statuses[i % 4], "checked_at": now - (i % 5) * 1000,
|
| 232 |
-
"mode": "Simulation", "tools": i % 3, "version": {"value": ">=0.2.3", "source": "Dependency constraint"}}
|
| 233 |
-
for i, r in enumerate(ROWS) if r.get("kind") == "space"}
|
| 234 |
-
monkeypatch.setattr(space_checks, "inventory", lambda: evidence)
|
| 235 |
-
ctx = search_api.context()
|
| 236 |
-
cards = search_api.py_cards([r for r in ROWS if not r.get("private")], ctx)
|
| 237 |
-
with snapshot.use() as (_, conn):
|
| 238 |
-
for facet, value in [("health", "API checked"), ("health", "Check expired"), ("health", "Not checked"),
|
| 239 |
-
("mode", "Simulation"), ("tools", "Tools discovered"), ("oe", ">=0.2.3"),
|
| 240 |
-
("oe_source", "Dependency constraint")]:
|
| 241 |
-
q = search_api.Query(size=100, sel={facet: [value]})
|
| 242 |
-
actual, expected = search_api.sql_search(conn, q, ctx), search_api.py_search(cards, q)
|
| 243 |
-
assert actual["total"] == expected["total"]
|
| 244 |
-
assert actual["facets"] == expected["facets"]
|
| 245 |
-
assert [r["id"] for r in actual["rows"]] == [r["id"] for r in expected["matches"]]
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
# ── parity with the page's own filtering ─────────────────────────────────────
|
| 249 |
-
def random_queries(n: int, seed: int = 3) -> list[search_api.Query]:
|
| 250 |
-
rnd = random.Random(seed)
|
| 251 |
-
words = ["", "", "a", "te", "sql", "bench", "game ocr", "s0", "C++", "日本", "\"", "near", "x" * 41, "d1 tasks"]
|
| 252 |
-
out = []
|
| 253 |
-
for _ in range(n):
|
| 254 |
-
sel = {}
|
| 255 |
-
for k, vals in (("type", ["Neither", "Benchmark", "OpenEnv", "ORS", "RL Environment"]),
|
| 256 |
-
("size", ["Under 100", "100 to 1,000", "1,000 or more", "Not indexed yet"]),
|
| 257 |
-
("stage", ["Running", "Sleeping", "Error", "Unknown"]), ("mcp", ["MCP tagged", "No MCP tag"]),
|
| 258 |
-
("evidence", ["Manifest present", "Hub tag only"]),
|
| 259 |
-
("oe", ["0.2.1", "0.2.3"]), ("tags", TAGS)):
|
| 260 |
-
if rnd.random() < 0.2:
|
| 261 |
-
sel[k] = rnd.sample(vals, rnd.randint(1, 2))
|
| 262 |
-
out.append(search_api.Query(q=rnd.choice(words), kind=rnd.choice(search_api.KINDS), sort=rnd.choice(search_api.SORTS),
|
| 263 |
-
coll=rnd.choice([None, None, "fineenvs", "terminal", "other"]), sel=sel))
|
| 264 |
-
return out
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
@pytest.mark.parametrize("mode", ["snapshot", "live"])
|
| 268 |
-
def test_parity_with_the_pages_filtering(tmp_path, mode):
|
| 269 |
-
if mode == "snapshot":
|
| 270 |
-
publish_fixture(tmp_path)
|
| 271 |
-
ctx = search_api.context()
|
| 272 |
-
cards = search_api.py_cards([r for r in ROWS if not r.get("private")], ctx)
|
| 273 |
-
with snapshot.use() as (snap, conn):
|
| 274 |
-
assert snap.source == mode
|
| 275 |
-
for q in random_queries(300):
|
| 276 |
-
got = search_api.sql_search(conn, q, ctx, limit=1000, offset=0)
|
| 277 |
-
want = search_api.py_search(cards, q)
|
| 278 |
-
assert [r["key"] for r in got["rows"]] == [d["key"] for d in want["matches"]], q
|
| 279 |
-
assert got["total"] == want["total"], q
|
| 280 |
-
for k in ("kind", "collection", *search_api.FACETS):
|
| 281 |
-
assert got["facets"][k] == want["facets"][k], (q, k)
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
def test_api_facets_match_the_reference(tmp_path):
|
| 285 |
-
publish_fixture(tmp_path)
|
| 286 |
-
ctx = search_api.context()
|
| 287 |
-
cards = search_api.py_cards([r for r in ROWS if not r.get("private")], ctx)
|
| 288 |
-
for qs, q in [("f=tags:sql|code;type:Neither", search_api.Query(sel={"tags": ["sql", "code"], "type": ["Neither"]})),
|
| 289 |
-
("k=openenv&f=stage:Running&s=updated", search_api.Query(kind="openenv", sel={"stage": ["Running"]}, sort="updated")),
|
| 290 |
-
("c=other&q=game&sort=new", search_api.Query(coll="other", q="game", sort="new")),
|
| 291 |
-
("f=tags:a%22quote", search_api.Query(sel={"tags": ['a"quote']}))]:
|
| 292 |
-
d = client.get(f"/api/search?{qs}&size=100").json()
|
| 293 |
-
want = search_api.py_search(cards, q)
|
| 294 |
-
assert d["total"] == want["total"] and [r["key"] for r in d["rows"]] == [x["key"] for x in want["matches"]][:100]
|
| 295 |
-
for k in search_api.FACETS:
|
| 296 |
-
assert d["facets"][k] == search_api._rank(want["facets"][k]), k
|
| 297 |
-
assert d["facets"]["collection"] == want["facets"]["collection"]
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
def test_paging_covers_every_match_once(tmp_path):
|
| 301 |
-
publish_fixture(tmp_path)
|
| 302 |
-
total = client.get("/api/search?facets=0").json()["total"]
|
| 303 |
-
seen = []
|
| 304 |
-
for page in range(1, 20):
|
| 305 |
-
rows = client.get(f"/api/search?sort=likes&page={page}&size=7&facets=0").json()["rows"]
|
| 306 |
-
if not rows:
|
| 307 |
-
break
|
| 308 |
-
seen += [r["key"] for r in rows]
|
| 309 |
-
assert len(seen) == len(set(seen)) == total
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
# ── search text is data, never FTS syntax ────────────────────────────────────
|
| 313 |
-
NASTY = ['"', '""', '"unbalanced', 'NEAR(a b)', 'NEAR(sql, 2)', 'a*', '*', '^sql', 'title:sql', 'brief:x', '{id heading}:sql',
|
| 314 |
-
'AND', 'OR', 'NOT', 'sql OR math', 'sql NOT math', '(sql', 'sql)', '-sql', '+sql', "'; DROP TABLE envs; --",
|
| 315 |
-
'\x00sql', 'c++', '日本語', '%', '_', '\\', 'a" OR "b', 'sql"*', ':', '"" ""', 'NEAR', 'near(']
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
@pytest.mark.parametrize("text", NASTY)
|
| 319 |
-
def test_search_text_cant_inject(tmp_path, text):
|
| 320 |
-
publish_fixture(tmp_path)
|
| 321 |
-
r = client.get("/api/search", params={"q": text, "size": 100})
|
| 322 |
-
assert r.status_code == 200
|
| 323 |
-
q = search_api.Query(q=text.strip())
|
| 324 |
-
want = search_api.py_search(search_api.py_cards([x for x in ROWS if not x.get("private")], search_api.context()), q)
|
| 325 |
-
assert [x["key"] for x in r.json()["rows"]] == [d["key"] for d in want["matches"]]
|
| 326 |
-
t = client.get("/api/search/tasks", params={"q": text})
|
| 327 |
-
assert t.status_code == 200 and isinstance(t.json()["rows"], list)
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
def test_fts_expressions_are_quoted():
|
| 331 |
-
assert search_api.envs_fts_query(['a"b', "near(x", "ab"]) == '"a""b" "near(x"'
|
| 332 |
-
assert search_api.tasks_fts_query('NEAR(sql OR "x*" title:join ^z)') == '"near"* "sql"* "or"* "x"* "title"* "join"* "z"*'
|
| 333 |
-
assert search_api.tasks_fts_query('"*" -- ()') is None
|
| 334 |
-
assert search_api.tasks_fts_query("a " * 50).count("*") == 8
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
def test_task_search_operators_are_words(tmp_path):
|
| 338 |
-
publish_fixture(tmp_path)
|
| 339 |
-
assert [r["ref"] for r in client.get("/api/search/tasks?q=foo").json()["rows"]] == ["tasks/foo-only"]
|
| 340 |
-
assert client.get("/api/search/tasks?q=foo OR join").json()["total"] == 0 # OR is a word to find, not an operator
|
| 341 |
-
assert client.get("/api/search/tasks?q=NEAR(foo bar)").json()["total"] == 0 # nor NEAR ("near" isn't in it)
|
| 342 |
-
assert client.get("/api/search/tasks?q=NOT foo").json()["total"] == 1 # "not" finds "Nothing": a word
|
| 343 |
-
assert client.get("/api/search/tasks?q=jo").json()["total"] == 1 # prefixes
|
| 344 |
-
assert client.get("/api/search/tasks?q=join&env=ds/d01").json()["total"] == 0
|
| 345 |
-
assert client.get("/api/search/tasks?q=join&env=../x").status_code == 400
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
# ── the visitor's own, headers ───────────────────────────────────────────────
|
| 349 |
-
def test_mine_merges_private_datasets(tmp_path, monkeypatch):
|
| 350 |
-
publish_fixture(tmp_path)
|
| 351 |
-
mine = [{"id": "me/private-ds", "key": "me/private-ds", "kind": "dataset", "framework": "harbor", "private": True, "heading": "Mine",
|
| 352 |
-
"brief": "my sql tasks", "tags": ["sql"], "badges": [], "downloads": 0, "likes": 0, "trending": 50, "indexed": {"tasks": 3}},
|
| 353 |
-
{**next(r for r in ROWS if r["id"] == "ds/d02"), "mine": True}]
|
| 354 |
-
monkeypatch.setattr(search_api, "_mine_rows", lambda request: mine)
|
| 355 |
-
d = client.get("/api/search?mine=1&c=mine&size=100").json()
|
| 356 |
-
assert {r["key"] for r in d["rows"]} == {"me/private-ds", "ds/d02"} and d["total"] == 2
|
| 357 |
-
assert d["facets"]["collection"]["mine"] == 2 and d["mine"] == 2
|
| 358 |
-
top = client.get("/api/search?mine=1&trending=3").json()
|
| 359 |
-
assert top["trending"][0]["key"] == "me/private-ds"
|
| 360 |
-
assert [r["key"] for r in top["featured"]] == PINS
|
| 361 |
-
own = client.get("/api/search?mine=1&c=mine&trending=24").json()
|
| 362 |
-
assert {r["key"] for r in own["trending"]} == {"me/private-ds", "ds/d02"}
|
| 363 |
-
assert top["total"] == len(visible()) + 1 and top["stats"]["datasets"] == sum(1 for r in visible() if r["kind"] == "dataset") + 1
|
| 364 |
-
assert next(r for r in top["rows"] if r["key"] == "me/private-ds")["private"] is True
|
| 365 |
-
r = client.get("/api/search?mine=1")
|
| 366 |
-
assert r.headers["cache-control"].startswith("private") and r.headers["vary"] == "Cookie"
|
| 367 |
-
monkeypatch.setattr(search_api, "_mine_rows", lambda request: None) # signed out: "mine" is no filter
|
| 368 |
-
assert client.get("/api/search?mine=1&c=mine").json()["query"]["collection"] is None
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
def test_cache_headers_and_etag(tmp_path):
|
| 372 |
-
publish_fixture(tmp_path)
|
| 373 |
-
r = client.get("/api/search?q=sql")
|
| 374 |
-
assert r.headers["cache-control"].startswith("public, max-age=") and r.headers["etag"].startswith('W/"')
|
| 375 |
-
again = client.get("/api/search?q=sql", headers={"If-None-Match": r.headers["etag"]})
|
| 376 |
-
assert again.status_code == 304 and not again.content
|
| 377 |
-
assert client.get("/api/search/tasks?q=sql").headers["cache-control"].startswith("public")
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
def test_responses_are_cards_only(tmp_path):
|
| 381 |
-
publish_fixture(tmp_path)
|
| 382 |
-
d = client.get("/api/search?trending=24").json()
|
| 383 |
-
assert len(d["rows"]) == 40 and len(d["trending"]) == 24
|
| 384 |
-
assert set(d["rows"][0]) == {"id", "key", "kind", "framework", "fw", "openenv", "collection", "heading", "brief", "downloads", "likes",
|
| 385 |
-
"trending", "updated", "created", "stage", "mcp", "openenv_version", "manifest", "hardware", "badges",
|
| 386 |
-
"tags", "pinned", "mine", "indexed", "rollouts", "private", "evidence", "stage_label",
|
| 387 |
-
"api_check", "api_status", "api_mode", "declared_version", "version_source", "tools_status"}
|
| 388 |
-
assert [c["id"] for c in d["collections"]] == ["fineenvs", "terminal"] and "ids" not in d["collections"][0]
|
| 389 |
-
assert client.get("/api/search?q=ds/d03").json()["rows"][0]["rollouts"] == 4
|
| 390 |
-
assert len(client.get("/api/search?trending=24").content) < 60_000
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
def test_rollouts_sort(tmp_path):
|
| 394 |
-
publish_fixture(tmp_path)
|
| 395 |
-
rows = client.get("/api/search?sort=rollouts&size=3").json()["rows"]
|
| 396 |
-
assert [r["id"] for r in rows] == ["ds/d03", "sp/s01", "ds/d05"]
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
def teardown_module(module):
|
| 400 |
-
shutil.rmtree(Path(config.STORAGE_DIR) / "snapshots", ignore_errors=True)
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
def test_environments_helper_for_whole_listing_readers(tmp_path, monkeypatch):
|
| 404 |
-
publish_fixture(tmp_path)
|
| 405 |
-
rows = search_api.environments()
|
| 406 |
-
assert [r["key"] for r in rows] == [r["key"] for r in ROWS if r["key"] not in HIDDEN and not r.get("private")] # listing order
|
| 407 |
-
by = {r["key"]: r for r in rows}
|
| 408 |
-
assert by["space:fe/plain"]["framework"] == "space" and by["space:fe/plain"]["collection"] == "fineenvs"
|
| 409 |
-
assert by["ds/d07"]["pinned"] and not by["ds/d07"]["gated"] and not by["ds/d07"]["private"]
|
| 410 |
-
assert {r["key"] for r in search_api.environments(include_hidden=True)} >= HIDDEN
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
def test_the_visitors_datasets_never_make_a_search_wait(monkeypatch):
|
| 414 |
-
from app import auth
|
| 415 |
-
|
| 416 |
-
calls = []
|
| 417 |
-
|
| 418 |
-
def mine(token):
|
| 419 |
-
calls.append(token)
|
| 420 |
-
return [{"id": f"me/v{len(calls)}"}]
|
| 421 |
-
|
| 422 |
-
monkeypatch.setattr(auth, "current_user", lambda request: {"name": "me", "token": "hf_secret_token"})
|
| 423 |
-
monkeypatch.setattr(catalog, "mine", mine)
|
| 424 |
-
search_api._mine.clear()
|
| 425 |
-
assert search_api._mine_rows(None)[0]["id"] == "me/v1" and search_api._mine_rows(None)[0]["id"] == "me/v1" and len(calls) == 1
|
| 426 |
-
who = next(iter(search_api._mine))
|
| 427 |
-
assert "hf_secret_token" not in who
|
| 428 |
-
search_api._mine[who] = (search_api._mine[who][0] - search_api.MINE_FRESH - 1, search_api._mine[who][1])
|
| 429 |
-
assert search_api._mine_rows(None)[0]["id"] == "me/v1" # stale: answered at once...
|
| 430 |
-
for _ in range(100):
|
| 431 |
-
if search_api._mine[who][1][0]["id"] == "me/v2":
|
| 432 |
-
break
|
| 433 |
-
time.sleep(0.02)
|
| 434 |
-
assert search_api._mine[who][1][0]["id"] == "me/v2" # ...and refreshed behind it
|
| 435 |
-
monkeypatch.setattr(auth, "current_user", lambda request: None)
|
| 436 |
-
assert search_api._mine_rows(None) is None
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
def test_a_spaces_rank_matches_the_trending_order_without_the_listing(tmp_path):
|
| 440 |
-
publish_fixture(tmp_path)
|
| 441 |
-
key = lambda d: (-d["trending"], -d["likes"], -(d.get("downloads") or 0))
|
| 442 |
-
spaces = [r for r in visible() if r["kind"] == "space"]
|
| 443 |
-
order = sorted(spaces, key=key)
|
| 444 |
-
for i in (0, len(order) // 2, len(order) - 1):
|
| 445 |
-
r = client.get(f"/api/search/rank?key={order[i]['key']}")
|
| 446 |
-
assert r.status_code == 200 and r.json() == {"n": i + 1, "of": len(order)}, order[i]["key"]
|
| 447 |
-
assert client.get(f"/api/search/rank?key={next(iter(HIDDEN))}").status_code == 404 # hidden: not ranked
|
| 448 |
-
assert client.get("/api/search/rank?key=space:nobody/nothing").status_code == 404
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
def test_runtime_and_evidence_filters_and_census_agree(tmp_path):
|
| 452 |
-
publish_fixture(tmp_path)
|
| 453 |
-
d = client.get("/api/search?kind=unverified-space&size=100").json()
|
| 454 |
-
assert sum(d["stats"]["openenv"].values()) == d["total"]
|
| 455 |
-
for evidence, count in d["stats"]["openenv"].items():
|
| 456 |
-
r = client.get("/api/search", params={"kind": "unverified-space", "f": f"evidence:{evidence}", "trending": 48, "size": 100}).json()
|
| 457 |
-
assert r["total"] == count
|
| 458 |
-
assert all(x["evidence"] == evidence for x in r["rows"] + r["trending"])
|
| 459 |
-
running = client.get("/api/search?kind=unverified-space&f=stage:Running").json()
|
| 460 |
-
assert running["total"] == d["stats"]["openenv_running"]
|
| 461 |
-
errors = client.get("/api/search?f=stage:Error&size=100").json()
|
| 462 |
-
assert errors["total"] and all(r["stage"] == "RUNTIME_ERROR" and r["stage_label"] == "Error" for r in errors["rows"])
|
| 463 |
-
sleeping = client.get("/api/search?f=stage:Sleeping&size=100").json()
|
| 464 |
-
assert sleeping["total"] and all(r["stage"] == "SLEEPING" for r in sleeping["rows"])
|
| 465 |
-
old = client.get("/api/search?f=stage:Asleep%20or%20stopped;mcp:Has%20MCP%20tools").json()
|
| 466 |
-
assert "Error" in old["query"]["f"]["stage"] and old["query"]["f"]["mcp"] == ["MCP tagged"]
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
def test_trending_priorities_cannot_overflow_into_each_other(tmp_path, monkeypatch):
|
| 470 |
-
# Old 1e9/1e4 weights let popularity displace a larger recent-interest score,
|
| 471 |
-
# and capped downloads made 10k and 10M downloads indistinguishable.
|
| 472 |
-
custom = [dict(ROWS[0], id=f"rank/{i}", key=f"rank/{i}", trending=t, likes=l, downloads=d)
|
| 473 |
-
for i, (t, l, d) in enumerate([(0, 10**9, 10**12), (0.1, 0, 0), (1, 3, 10000), (1, 3, 10**7)])]
|
| 474 |
-
monkeypatch.setattr(__import__(__name__, fromlist=["ROWS"]), "ROWS", custom)
|
| 475 |
-
monkeypatch.setattr(catalog, "pinned", lambda: ["rank/0"])
|
| 476 |
-
publish_fixture(tmp_path)
|
| 477 |
-
got = client.get("/api/search?trending=4").json()
|
| 478 |
-
assert [r["id"] for r in got["rows"]] == ["rank/3", "rank/2", "rank/1", "rank/0"]
|
| 479 |
-
assert got["trending"] == got["rows"]
|
| 480 |
-
assert [r["id"] for r in got["featured"]] == ["rank/0"]
|
| 481 |
-
assert got["ranking"]["order"] == ["trending", "likes", "downloads"]
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
def test_empty_filters_have_no_unrelated_trending_or_featured(tmp_path):
|
| 485 |
-
publish_fixture(tmp_path)
|
| 486 |
-
d = client.get("/api/search?q=absent-environment-xyz&trending=24").json()
|
| 487 |
-
assert d["total"] == 0 and d["trending"] == [] and d["featured"] == []
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
@pytest.mark.parametrize("mode", ["published", "fallback"])
|
| 491 |
-
def test_owner_filter_is_exact_and_shared_by_results_facets_and_trending(tmp_path, monkeypatch, mode):
|
| 492 |
-
import sys
|
| 493 |
-
space = next(r for r in ROWS if r["kind"] == "space")
|
| 494 |
-
dataset = ROWS[0]
|
| 495 |
-
mixed = [*ROWS, {**space, "id": "ds/owned-space", "key": "space:ds/owned-space"},
|
| 496 |
-
{**dataset, "id": "ds-copy/impostor", "key": "ds-copy/impostor", "heading": "ds environments"}]
|
| 497 |
-
monkeypatch.setattr(sys.modules[__name__], "ROWS", mixed)
|
| 498 |
-
if mode == "published":
|
| 499 |
-
publish_fixture(tmp_path)
|
| 500 |
-
expected = {r["key"] for r in visible() if r["id"].split("/")[0] == "ds"}
|
| 501 |
-
data = client.get("/api/search", params={"owner": "DS", "size": 100, "trending": 48}).json()
|
| 502 |
-
assert {r["key"] for r in data["rows"]} == expected
|
| 503 |
-
assert {r["kind"] for r in data["rows"]} == {"dataset", "space"}
|
| 504 |
-
assert data["total"] == data["facets"]["kind"]["all"] == len(expected)
|
| 505 |
-
assert {r["key"] for r in data["trending"]} == expected
|
| 506 |
-
assert all(r["id"].split("/")[0] == "ds" for r in data["featured"])
|
| 507 |
-
query = search_api.Query(owner="DS")
|
| 508 |
-
ref = search_api.py_search(search_api.py_cards([r for r in mixed if not r.get("private")], search_api.context()), query)
|
| 509 |
-
assert {r["key"] for r in ref["matches"]} == expected
|
| 510 |
-
pages = [client.get("/api/search", params={"owner": "ds", "size": 10, "page": page, "facets": 0}).json()["rows"]
|
| 511 |
-
for page in range(1, (len(expected) + 9) // 10 + 1)]
|
| 512 |
-
assert {r["key"] for page in pages for r in page} == expected
|
| 513 |
-
assert sum(map(len, pages)) == len(expected)
|
| 514 |
-
filtered = client.get("/api/search", params={"owner": "ds", "kind": "openenv", "q": "owned-space"}).json()
|
| 515 |
-
assert all(r["id"] == "ds/owned-space" for r in filtered["rows"])
|
| 516 |
-
for owner in ("d", "ds%", "ds/", "missing", "ds' OR 1=1 --"):
|
| 517 |
-
empty = client.get("/api/search", params={"owner": owner, "trending": 24}).json()
|
| 518 |
-
assert empty["total"] == 0 and empty["trending"] == [] and empty["featured"] == []
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
def test_owner_filter_applies_to_authorized_private_dataset_merge(tmp_path, monkeypatch):
|
| 522 |
-
publish_fixture(tmp_path)
|
| 523 |
-
mine = [{**ROWS[0], "id": "me/private", "key": "me/private", "private": True},
|
| 524 |
-
{**ROWS[0], "id": "someone-else/private", "key": "someone-else/private", "private": True}]
|
| 525 |
-
monkeypatch.setattr(search_api, "_mine_rows", lambda request: mine)
|
| 526 |
-
public = client.get("/api/search", params={"owner": "me"}).json()
|
| 527 |
-
assert public["total"] == 0
|
| 528 |
-
response = client.get("/api/search", params={"owner": "ME", "mine": 1, "trending": 24})
|
| 529 |
-
data = response.json()
|
| 530 |
-
assert data["total"] == 1
|
| 531 |
-
assert data["stats"]["datasets"] == 1
|
| 532 |
-
assert data["stats"]["dataset_tasks"] == (mine[0].get("indexed") or {}).get("tasks", 0)
|
| 533 |
-
assert [r["id"] for r in data["rows"]] == [r["id"] for r in data["trending"]] == ["me/private"]
|
| 534 |
-
assert data["rows"][0]["private"] is True
|
| 535 |
-
assert response.headers["cache-control"].startswith("private")
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
@pytest.mark.parametrize("status,stage,age,expected", [
|
| 539 |
-
(None, "RUNNING", 0, "unverified-space"),
|
| 540 |
-
("Checks failed", "RUNNING", 0, "unverified-space"),
|
| 541 |
-
("API checked", "RUNNING", 0, "openenv"),
|
| 542 |
-
("API checked", "RUNNING", 3601, "unverified-space"),
|
| 543 |
-
("API checked", "SLEEPING", 0, "unverified-space"),
|
| 544 |
-
])
|
| 545 |
-
def test_manifest_filename_never_establishes_openenv_without_fresh_live_evidence(tmp_path, monkeypatch, status, stage, age, expected):
|
| 546 |
-
from app import space_checks
|
| 547 |
-
publish_fixture(tmp_path)
|
| 548 |
-
target = next(r for r in ROWS if r.get("manifest"))
|
| 549 |
-
record = {"id": target["id"], "status": status, "stage": stage, "checked_at": time.time() - age}
|
| 550 |
-
monkeypatch.setattr(space_checks, "inventory", lambda: {target["id"]: record} if status else {})
|
| 551 |
-
data = client.get("/api/search", params={"q": target["id"], "trending": 24}).json()
|
| 552 |
-
row = next(r for r in data["rows"] if r["id"] == target["id"])
|
| 553 |
-
assert row["fw"] == expected
|
| 554 |
-
assert row["openenv"] is (expected == "openenv")
|
| 555 |
-
if expected != "openenv":
|
| 556 |
-
assert row["framework"] == "space" and "OpenEnv" not in row["badges"]
|
| 557 |
-
filtered = client.get("/api/search", params={"q": target["id"], "kind": "openenv", "trending": 24}).json()
|
| 558 |
-
assert bool(filtered["rows"]) is (expected == "openenv")
|
| 559 |
-
assert bool(filtered["trending"]) is (expected == "openenv")
|
| 560 |
-
assert filtered["facets"]["kind"].get(expected, 0) > 0
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
def test_verified_untagged_space_has_protocol_facets(tmp_path, monkeypatch):
|
| 564 |
-
from app import space_checks
|
| 565 |
-
publish_fixture(tmp_path)
|
| 566 |
-
record = {"id": "fe/plain", "status": "API checked", "stage": "RUNNING", "checked_at": time.time(), "mode": "Simulation", "tools": 0}
|
| 567 |
-
monkeypatch.setattr(space_checks, "inventory", lambda: {"fe/plain": record})
|
| 568 |
-
data = client.get("/api/search", params={"kind": "openenv", "f": "health:API%20checked"}).json()
|
| 569 |
-
assert [r["id"] for r in data["rows"]] == ["fe/plain"]
|
| 570 |
-
ctx = search_api.context()
|
| 571 |
-
cards = search_api.py_cards([r for r in ROWS if not r.get("private")], ctx)
|
| 572 |
-
with snapshot.use() as (_, conn):
|
| 573 |
-
q = search_api.Query(kind="openenv")
|
| 574 |
-
assert search_api.sql_search(conn, q, ctx)["facets"] == search_api.py_search(cards, q)["facets"]
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
def test_ready_scope_counts_and_facets_share_live_evidence(tmp_path, monkeypatch):
|
| 578 |
-
from app import space_checks
|
| 579 |
-
publish_fixture(tmp_path)
|
| 580 |
-
now = time.time()
|
| 581 |
-
good = {"id": "fe/geo", "status": "API checked", "stage": "RUNNING", "checked_at": now,
|
| 582 |
-
"task_catalog": {"checked_at": now, "tasks": 1234, "counted_splits": 1, "complete": True}}
|
| 583 |
-
checks = {"fe/geo": good,
|
| 584 |
-
"fe/latex": {**good, "id": "fe/latex", "checked_at": now - 3601, "task_catalog": {}},
|
| 585 |
-
"fe/plain": {**good, "id": "fe/plain", "status": "Checks failed", "task_catalog": {}},
|
| 586 |
-
"fe/ors-one": {**good, "id": "fe/ors-one", "status": "Checks failed", "interface": "ors", "task_catalog": {}}}
|
| 587 |
-
monkeypatch.setattr(space_checks, "inventory", lambda: checks)
|
| 588 |
-
data = client.get("/api/search", params={"scope": "ready", "owner": "fe", "size": 100, "trending": 24}).json()
|
| 589 |
-
assert {r["id"] for r in data["rows"]} == {"fe/geo", "fe/ors-one"}
|
| 590 |
-
assert data["stats"]["datasets"] == data["stats"]["dataset_tasks"] == 0
|
| 591 |
-
assert data["stats"]["spaces"] == data["total"] == data["facets"]["kind"]["all"] == 2
|
| 592 |
-
assert data["stats"]["space_tasks"] == 1234
|
| 593 |
-
rank = client.get("/api/search/rank", params={"scope": "ready", "key": "space:fe/geo"}).json()
|
| 594 |
-
assert rank["of"] == 2
|
| 595 |
-
assert client.get("/api/search/rank", params={"scope": "ready", "key": "space:fe/plain"}).status_code == 404
|
| 596 |
-
ctx = search_api.context()
|
| 597 |
-
cards = search_api.py_cards([r for r in ROWS if not r.get("private")], ctx)
|
| 598 |
-
with snapshot.use() as (_, conn):
|
| 599 |
-
for q in (search_api.Query(scope="ready"), search_api.Query(scope="ready", owner="fe"),
|
| 600 |
-
search_api.Query(scope="ready", kind="openenv")):
|
| 601 |
-
sql = search_api.sql_search(conn, q, ctx, limit=100)
|
| 602 |
-
py = search_api.py_search(cards, q)
|
| 603 |
-
assert sql["total"] == py["total"]
|
| 604 |
-
assert sql["facets"] == py["facets"]
|
| 605 |
-
datasets = client.get("/api/search", params={"scope": "ready", "owner": "ds"}).json()
|
| 606 |
-
assert datasets["stats"]["datasets"] == datasets["total"] and datasets["stats"]["spaces"] == 0
|
| 607 |
-
assert datasets["stats"]["dataset_tasks"] == len(TASKS["ds/d00"])
|
| 608 |
-
empty = client.get("/api/search", params={"scope": "ready", "q": "nothing-matches"}).json()
|
| 609 |
-
assert all(empty["stats"][k] == 0 for k in ("datasets", "spaces", "dataset_tasks", "space_tasks"))
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
def test_fineenvs_promoted_without_pins_and_without_changing_trending(tmp_path, monkeypatch):
|
| 613 |
-
import sys
|
| 614 |
-
from app import space_checks
|
| 615 |
-
custom = [{**ROWS[0], "id": "FineEnvs/data", "key": "FineEnvs/data", "trending": 0},
|
| 616 |
-
{**ROWS[1], "id": "other/popular", "key": "other/popular", "trending": 100},
|
| 617 |
-
{**ROWS[-2], "id": "FineEnvs/server", "key": "space:FineEnvs/server", "kind": "space", "trending": 0}]
|
| 618 |
-
monkeypatch.setattr(sys.modules[__name__], "ROWS", custom)
|
| 619 |
-
monkeypatch.setattr(catalog, "pinned", list)
|
| 620 |
-
monkeypatch.setattr(space_checks, "inventory", lambda: {"FineEnvs/server": {
|
| 621 |
-
"id": "FineEnvs/server", "status": "API checked", "stage": "RUNNING", "checked_at": time.time()}})
|
| 622 |
-
publish_fixture(tmp_path)
|
| 623 |
-
data = client.get("/api/search?scope=ready&trending=24").json()
|
| 624 |
-
assert [r["id"] for r in data["promoted"]] == ["FineEnvs/server", "FineEnvs/data"]
|
| 625 |
-
assert data["trending"][0]["id"] == "other/popular"
|
| 626 |
-
assert data["featured"] == []
|
| 627 |
-
assert client.get("/api/search?scope=ready&owner=other&trending=24").json()["promoted"] == []
|
| 628 |
-
assert client.get("/api/search?scope=ready&q=popular&trending=24").json()["promoted"] == []
|
| 629 |
-
assert len(client.get("/api/search?scope=ready&owner=fineenvs&trending=24").json()["promoted"]) == 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_security.py
DELETED
|
@@ -1,554 +0,0 @@
|
|
| 1 |
-
"""Security properties of the explorer, end to end through the HTTP API (no network: the Hub is stubbed).
|
| 2 |
-
|
| 3 |
-
uv run pytest tests -q
|
| 4 |
-
|
| 5 |
-
What is checked: who may see and change what (admins, owners, everyone), that sessions can't be forged, that
|
| 6 |
-
cross-site requests are refused, that private datasets and private rollouts stay private, that answers and tokens
|
| 7 |
-
never reach a page, that a visitor's endpoint can't point the server at a private address, and the headers.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import json
|
| 13 |
-
import os
|
| 14 |
-
import tempfile
|
| 15 |
-
import time
|
| 16 |
-
from pathlib import Path
|
| 17 |
-
|
| 18 |
-
import pytest
|
| 19 |
-
|
| 20 |
-
_TMP = os.environ.get("RLX_TEST_TMP") or tempfile.mkdtemp(prefix="rlx-test-") # conftest.py's, shared by every module
|
| 21 |
-
os.environ.update(STORAGE_DIR=_TMP, RLX_CACHE_DIR=f"{_TMP}/cache", OAUTH_CLIENT_ID="test-client", SESSION_SECRET="test-secret",
|
| 22 |
-
RLX_WARM="0", RLX_ADMIN_ORG="FineEnvs")
|
| 23 |
-
|
| 24 |
-
from fastapi.testclient import TestClient # noqa: E402
|
| 25 |
-
|
| 26 |
-
from app import admin_app, auth, catalog, config, endpoints, main, models, runner, settings, store # noqa: E402
|
| 27 |
-
|
| 28 |
-
TOKENS = {"alice": "hf_alice_secret_token_0001", "bob": "hf_bob_secret_token_0002"}
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def cookie(name: str, exp: float | None = None) -> dict[str, str]:
|
| 32 |
-
session = {"token": TOKENS[name], "name": name, "avatar": None, "via": "token", "exp": exp or time.time() + 3600}
|
| 33 |
-
return {"Cookie": f"{auth.COOKIE}={auth._box.encrypt(json.dumps(session).encode()).decode()}"}
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
@pytest.fixture(autouse=True)
|
| 37 |
-
def stubs(monkeypatch):
|
| 38 |
-
"""No Hub: alice is a FineEnvs member, bob isn't; the catalog is empty."""
|
| 39 |
-
monkeypatch.setattr(settings, "_org_members", lambda: {"alice"})
|
| 40 |
-
monkeypatch.setattr(catalog, "environments", lambda include_hidden=False: [])
|
| 41 |
-
monkeypatch.setattr(models, "get", lambda mid: {"id": mid, "tools": True, "provider": "x"})
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
client = TestClient(main.app, base_url="https://testserver")
|
| 45 |
-
admin = TestClient(admin_app.app, base_url="https://testserver") # its own app, on its own private Space
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def runnable_task(monkeypatch, **row):
|
| 49 |
-
"""A task any environment's adapter says runs with the Harbor runner (app/envs/registry.run_task, stubbed)."""
|
| 50 |
-
from types import SimpleNamespace
|
| 51 |
-
|
| 52 |
-
from app.envs import registry
|
| 53 |
-
|
| 54 |
-
row = {"title": "T", "restricted": False, "sha": "abc", "runnable": {"ok": True, "how": "image"}, "image": "python:3.12", **row}
|
| 55 |
-
opts = [{"runner": "harbor", "label": "Harbor", "ok": True, "why": "", "fields": [], "harnesses": None}]
|
| 56 |
-
monkeypatch.setattr(registry, "run_task", lambda d, p, t=None: (SimpleNamespace(key=d, adapter=SimpleNamespace(id="test")), row, opts))
|
| 57 |
-
monkeypatch.setattr(runner.Rollout, "execute", lambda self: None)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
def run(**kw) -> dict:
|
| 61 |
-
rid = kw.pop("id", f"20260101-000000-{os.urandom(3).hex()}")
|
| 62 |
-
base = {"id": rid, "user": "alice", "dataset": "org/ds", "path": "tasks/t1", "task_id": "org/ds:tasks/t1", "title": "T",
|
| 63 |
-
"model": "m/x", "harness": "opencode", "status": "done", "reward": 1.0, "visibility": "public"}
|
| 64 |
-
return store.create({**base, **kw})
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
# ── sessions ─────────────────────────────────────────────────────────────────
|
| 68 |
-
def test_signed_out_and_forged_sessions_are_nobody():
|
| 69 |
-
assert client.get("/api/runs").status_code == 401
|
| 70 |
-
assert client.get("/api/runs", headers={"Cookie": f"{auth.COOKIE}=gAAAAABforged"}).status_code == 401
|
| 71 |
-
assert client.get("/api/runs", headers=cookie("alice", exp=time.time() - 10)).status_code == 401
|
| 72 |
-
assert client.get("/api/runs", headers=cookie("alice")).status_code == 200
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
def test_me_never_returns_the_token():
|
| 76 |
-
body = client.get("/api/me", headers=cookie("alice")).text
|
| 77 |
-
assert TOKENS["alice"] not in body
|
| 78 |
-
assert json.loads(body)["user"]["name"] == "alice"
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
# ── admin ────────────────────────────────────────────────────────────────────
|
| 82 |
-
ADMIN_GETS = ["/api/admin/overview", "/api/admin/rollouts", "/api/admin/environments", "/api/admin/collections",
|
| 83 |
-
"/api/admin/indexes", "/api/admin/settings", "/api/admin/audit"]
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
@pytest.mark.parametrize("path", ADMIN_GETS)
|
| 87 |
-
def test_admin_is_for_org_members_only(path):
|
| 88 |
-
assert admin.get(path).status_code == 401
|
| 89 |
-
assert admin.get(path, headers=cookie("bob")).status_code == 403
|
| 90 |
-
assert admin.get(path, headers=cookie("alice")).status_code == 200
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
@pytest.mark.parametrize("path", ADMIN_GETS)
|
| 94 |
-
def test_the_public_explorer_has_no_admin(path):
|
| 95 |
-
r = client.get(path, headers=cookie("alice"))
|
| 96 |
-
assert r.status_code == 404 or "Admin" not in r.text # the static fallback serves nothing there
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
def test_admin_writes_refused_for_others_and_audited():
|
| 100 |
-
body = {"rollouts_enabled": True, "max_active": 5, "max_per_user": 2, "agents": ["opencode"], "announcement": "hi"}
|
| 101 |
-
assert admin.put("/api/admin/settings", json=body).status_code == 401
|
| 102 |
-
assert admin.put("/api/admin/settings", json=body, headers=cookie("bob")).status_code == 403
|
| 103 |
-
assert admin.post("/api/admin/environments", json={"key": "org/ds", "action": "hide"}, headers=cookie("bob")).status_code == 403
|
| 104 |
-
r = admin.put("/api/admin/settings", json=body, headers=cookie("alice"))
|
| 105 |
-
assert r.status_code == 200 and r.json()["max_active"] == 5
|
| 106 |
-
entry = settings.audit(1)[0]
|
| 107 |
-
assert entry["user"] == "alice" and entry["after"]["max_active"] == 5
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
def test_admin_input_is_validated():
|
| 111 |
-
h = cookie("alice")
|
| 112 |
-
assert admin.put("/api/admin/settings", json={"rollouts_enabled": True, "max_active": 0, "max_per_user": 2, "agents": []}, headers=h).status_code == 422
|
| 113 |
-
assert admin.put("/api/admin/settings", json={"rollouts_enabled": True, "max_active": 3, "max_per_user": 2, "agents": ["rm -rf"]}, headers=h).status_code == 400
|
| 114 |
-
bad_key = {"collections": [{"id": "x1", "group": "X", "icon": "grid", "color": "code", "ids": ["not a key"]}]}
|
| 115 |
-
assert admin.put("/api/admin/collections", json=bad_key, headers=h).status_code == 400
|
| 116 |
-
bad_id = {"collections": [{"id": "Bad Id", "group": "X", "icon": "grid", "color": "code", "ids": []}]}
|
| 117 |
-
assert admin.put("/api/admin/collections", json=bad_id, headers=h).status_code == 422
|
| 118 |
-
assert admin.post("/api/admin/environments", json={"key": "../../etc", "action": "pin"}, headers=h).status_code == 400
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
def test_session_name_cannot_be_claimed_by_a_header():
|
| 122 |
-
# the admin check reads the name from the encrypted session only
|
| 123 |
-
assert admin.get("/api/admin/settings", headers={**cookie("bob"), "X-User": "alice", "X-Forwarded-User": "alice"}).status_code == 403
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
# ── cross-site requests ──────────────────────────────────────────────────────
|
| 127 |
-
def test_cross_site_posts_are_refused():
|
| 128 |
-
h = cookie("alice")
|
| 129 |
-
r = client.post("/api/runs", json={"dataset": "org/ds"}, headers={**h, "Origin": "https://evil.example"})
|
| 130 |
-
assert r.status_code == 403
|
| 131 |
-
r = client.post("/api/runs", json={"dataset": "org/ds"}, headers={**h, "Sec-Fetch-Site": "cross-site"})
|
| 132 |
-
assert r.status_code == 403
|
| 133 |
-
r = admin.put("/api/admin/settings", json={}, headers={**h, "Origin": "https://evil.example"})
|
| 134 |
-
assert r.status_code == 403
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
# ── rollouts: who sees what ──────────────────────────────────────────────────
|
| 138 |
-
def test_private_rollouts_are_owner_only():
|
| 139 |
-
r = run(visibility="private")
|
| 140 |
-
assert client.get(f"/api/runs/{r['id']}").status_code == 404
|
| 141 |
-
assert client.get(f"/api/runs/{r['id']}", headers=cookie("bob")).status_code == 404
|
| 142 |
-
assert client.get(f"/api/runs/{r['id']}", headers=cookie("alice")).status_code == 200
|
| 143 |
-
assert client.post(f"/api/runs/{r['id']}/visibility", json={"visibility": "public"}, headers=cookie("bob")).status_code == 404
|
| 144 |
-
assert client.post(f"/api/runs/{r['id']}/cancel", headers=cookie("bob")).status_code == 404
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
def test_public_rollouts_hide_who_ran_them():
|
| 148 |
-
r = run(endpoint={"base_url": "https://llm.alice-corp.example/v1", "host": "llm.alice-corp.example", "model": "m"})
|
| 149 |
-
store.write_artifact(r["id"], "trajectory.json", json.dumps([{"role": "user", "content": "hello alice, use https://llm.alice-corp.example/v1"}]))
|
| 150 |
-
body = client.get(f"/api/runs/{r['id']}").text
|
| 151 |
-
data = json.loads(body)
|
| 152 |
-
assert "user" not in data["run"] and data["run"]["endpoint"] == {"custom": True}
|
| 153 |
-
assert "alice" not in body and "alice-corp" not in body
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
def test_ungraded_unfinished_and_restricted_rollouts_stay_off_community():
|
| 157 |
-
keep = run(dataset="org/community-check")
|
| 158 |
-
run(dataset="org/community-check", status="running", reward=None)
|
| 159 |
-
run(dataset="org/community-check", status="failed", reward=None)
|
| 160 |
-
run(dataset="org/community-check", restricted=True)
|
| 161 |
-
run(dataset="org/community-check", visibility="private")
|
| 162 |
-
ids = [x["id"] for x in client.get("/api/community?dataset=org/community-check").json()["runs"]]
|
| 163 |
-
assert ids == [keep["id"]]
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
def test_rollouts_on_private_datasets_cannot_go_public():
|
| 167 |
-
r = run(visibility="private", restricted=True)
|
| 168 |
-
assert client.post(f"/api/runs/{r['id']}/visibility", json={"visibility": "public"}, headers=cookie("alice")).status_code == 400
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
def test_tokens_never_reach_the_store(monkeypatch):
|
| 172 |
-
runnable_task(monkeypatch)
|
| 173 |
-
r = client.post("/api/runs", json={"dataset": "org/ds", "path": "tasks/t1", "model": "m/x"}, headers=cookie("alice"))
|
| 174 |
-
assert r.status_code == 200, r.text
|
| 175 |
-
assert TOKENS["alice"] not in r.text
|
| 176 |
-
for f in Path(_TMP).rglob("*"):
|
| 177 |
-
if f.is_file():
|
| 178 |
-
assert TOKENS["alice"].encode() not in f.read_bytes(), f
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
def test_paused_rollouts_are_refused(monkeypatch):
|
| 182 |
-
runnable_task(monkeypatch)
|
| 183 |
-
settings.save("alice", {"rollouts_enabled": False})
|
| 184 |
-
try:
|
| 185 |
-
r = client.post("/api/runs", json={"dataset": "org/ds", "path": "tasks/t1", "model": "m/x"}, headers=cookie("alice"))
|
| 186 |
-
assert r.status_code == 429 and "paused" in r.json()["detail"]
|
| 187 |
-
finally:
|
| 188 |
-
settings.save("alice", {"rollouts_enabled": True})
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
# ── private datasets ─────────────────────────────────────────────────────────
|
| 192 |
-
def test_private_datasets_need_a_token_that_can_read_them(monkeypatch):
|
| 193 |
-
calls = []
|
| 194 |
-
|
| 195 |
-
def fetch(spec, token): # the Hub: only alice's token can read it
|
| 196 |
-
calls.append(token)
|
| 197 |
-
if token == TOKENS["alice"]:
|
| 198 |
-
return {"id": spec, "sha": "abc", "restricted": True}
|
| 199 |
-
raise PermissionError("private")
|
| 200 |
-
|
| 201 |
-
monkeypatch.setattr(catalog, "_fetch_info", fetch)
|
| 202 |
-
catalog._memo.clear()
|
| 203 |
-
with pytest.raises(PermissionError):
|
| 204 |
-
catalog.info("org/private-ds")
|
| 205 |
-
assert catalog.info("org/private-ds", TOKENS["alice"])["restricted"]
|
| 206 |
-
with pytest.raises(PermissionError): # bob's token is checked on its own, never served alice's cached answer
|
| 207 |
-
catalog.info("org/private-ds", TOKENS["bob"])
|
| 208 |
-
with pytest.raises(PermissionError): # and signed out, still nothing
|
| 209 |
-
catalog.info("org/private-ds")
|
| 210 |
-
assert TOKENS["bob"] in calls
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
# ── answers stay out ─────────────────────────────────────────────────────────
|
| 214 |
-
@pytest.mark.parametrize("rel,text,withheld", [
|
| 215 |
-
("solution/solve.sh", None, True),
|
| 216 |
-
("tests/test.sh", "pytest /tests", False),
|
| 217 |
-
("tests/grade.py", "import json", False),
|
| 218 |
-
("tests/expected_output.json", None, True),
|
| 219 |
-
("tests/verifier/verifier_meta.json", None, True),
|
| 220 |
-
("tests/grade.json", None, True),
|
| 221 |
-
("tests/check.py", "GOLD = {'gold_answer': 42}", True),
|
| 222 |
-
("environment/setup/files/expected_func.json", None, True),
|
| 223 |
-
("instruction.md", "find the gold_answer", False),
|
| 224 |
-
])
|
| 225 |
-
def test_answers_are_withheld(rel, text, withheld):
|
| 226 |
-
assert catalog._withheld(rel, text) is withheld
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
def test_task_toml_masks_answer_keys():
|
| 230 |
-
out = catalog._masked_toml('[metadata]\ngold_answer = "42"\nexpected_output = "x"\ndifficulty = "easy"')
|
| 231 |
-
assert '"42"' not in out and '"x"' not in out and 'difficulty = "easy"' in out
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
def test_task_files_stay_inside_the_task(monkeypatch):
|
| 235 |
-
monkeypatch.setattr(catalog, "_task_row", lambda s, p, t=None: ({}, {"sha": "abc"}))
|
| 236 |
-
monkeypatch.setattr(catalog, "_task_texts", lambda s, sha, p, t: ({"tests/test.sh": 10, "solution/solve.sh": 5}, {"tests/test.sh": "echo"}, set()))
|
| 237 |
-
assert catalog.task_file("org/ds", "tasks/t1", "../t2/task.toml")["error"]
|
| 238 |
-
assert catalog.task_file("org/ds", "tasks/t1", "/etc/passwd")["error"]
|
| 239 |
-
assert catalog.task_file("org/ds", "tasks/t1", "solution/solve.sh").get("withheld")
|
| 240 |
-
assert catalog.task_file("org/ds", "tasks/t1", "tests/test.sh")["text"] == "echo"
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
def test_dataset_ids_are_checked():
|
| 244 |
-
for bad in ("../../etc", "a/b/c", "org/..", "", "org/ name"):
|
| 245 |
-
with pytest.raises(ValueError):
|
| 246 |
-
catalog.check_spec(bad)
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
# ── a visitor's endpoint ─────────────────────────────────────────────────────
|
| 250 |
-
@pytest.mark.parametrize("url", ["http://example.com/v1", "https://127.0.0.1/v1", "https://localhost/v1", "https://169.254.169.254/latest",
|
| 251 |
-
"https://10.0.0.5/v1", "https://[::1]/v1", "https://user:pass@example.com/v1"])
|
| 252 |
-
def test_endpoints_must_be_public_https(url):
|
| 253 |
-
with pytest.raises(endpoints.EndpointError):
|
| 254 |
-
endpoints.check_url(url)
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
def test_space_hosts_are_pinned_to_hf_space():
|
| 258 |
-
import re
|
| 259 |
-
|
| 260 |
-
pat = re.compile(r"^https://[a-z0-9-]+\.hf\.space$")
|
| 261 |
-
assert pat.match("https://openenv-coding-env.hf.space")
|
| 262 |
-
for bad in ("https://evil.com", "https://x.hf.space.evil.com", "https://x.hf.space/../", "http://x.hf.space"):
|
| 263 |
-
assert not pat.match(bad)
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
# ── headers ──────────────────────────────────────────────────────────────────
|
| 267 |
-
def test_security_headers():
|
| 268 |
-
r = client.get("/")
|
| 269 |
-
csp = r.headers["content-security-policy"]
|
| 270 |
-
assert "object-src 'none'" in csp and "frame-ancestors 'self' https://huggingface.co" in csp and "script-src 'self'" in csp
|
| 271 |
-
assert r.headers["x-content-type-options"] == "nosniff"
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
def test_local_mode_refuses_other_hosts(monkeypatch):
|
| 275 |
-
monkeypatch.setattr(config, "LOCAL_MODE", True)
|
| 276 |
-
assert client.get("/api/me", headers={"Host": "evil.example"}).status_code == 403
|
| 277 |
-
assert client.get("/api/me", headers={"Host": "localhost:8060"}).status_code == 200
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
# ── what the admin Space changes reaches the explorer through the bucket ─────
|
| 281 |
-
def test_admin_hides_a_rollout_from_community():
|
| 282 |
-
r = run(dataset="org/moderation-check")
|
| 283 |
-
assert [x["id"] for x in client.get("/api/community?dataset=org/moderation-check").json()["runs"]] == [r["id"]]
|
| 284 |
-
assert admin.post(f"/api/admin/rollouts/{r['id']}/moderate", json={"hidden": True}, headers=cookie("alice")).status_code == 200
|
| 285 |
-
assert client.get("/api/community?dataset=org/moderation-check").json()["runs"] == []
|
| 286 |
-
assert client.get(f"/api/runs/{r['id']}").status_code == 404 # gone for everyone
|
| 287 |
-
assert client.get(f"/api/runs/{r['id']}", headers=cookie("alice")).status_code == 200 # its owner still has it
|
| 288 |
-
admin.post(f"/api/admin/rollouts/{r['id']}/moderate", json={"hidden": False}, headers=cookie("alice"))
|
| 289 |
-
assert client.get(f"/api/runs/{r['id']}").status_code == 200
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
def test_admin_stop_requests_reach_running_rollouts(monkeypatch):
|
| 293 |
-
stopped = []
|
| 294 |
-
monkeypatch.setattr(runner, "is_live", lambda rid: True)
|
| 295 |
-
monkeypatch.setattr(runner, "cancel", lambda rid: stopped.append(rid) or True)
|
| 296 |
-
r = run(status="running", reward=None, updated_at=time.time())
|
| 297 |
-
assert admin.post(f"/api/admin/rollouts/{r['id']}/cancel", headers=cookie("alice")).status_code == 200
|
| 298 |
-
assert admin.post(f"/api/admin/rollouts/{r['id']}/cancel", headers=cookie("bob")).status_code == 403
|
| 299 |
-
import threading
|
| 300 |
-
|
| 301 |
-
threading.Thread(target=runner.watch_cancel_requests, daemon=True).start()
|
| 302 |
-
for _ in range(40):
|
| 303 |
-
if r["id"] in stopped:
|
| 304 |
-
break
|
| 305 |
-
time.sleep(0.1)
|
| 306 |
-
assert r["id"] in stopped
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
def test_answers_written_into_grader_scripts_are_masked():
|
| 310 |
-
sh = "expected_answer=$(cat <<'ANSWER_EOF'\n138236\nANSWER_EOF\n)\nEXPECTED=\"42\"\nagent=$(cat /workdir/answer.txt)\n"
|
| 311 |
-
out = catalog._mask_answers("tests/test.sh", sh)
|
| 312 |
-
assert "138236" not in out and '"42"' not in out and "cat /workdir/answer.txt" in out
|
| 313 |
-
py = 'GOLD_ANSWER = {"x": 1}\nresult = run()\n'
|
| 314 |
-
assert "{\"x\": 1}" not in catalog._mask_answers("tests/grade.py", py)
|
| 315 |
-
assert catalog._mask_answers("instruction.md", "expected = 5") == "expected = 5" # only grader files
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
def test_no_network_tasks_are_refused_with_a_reason():
|
| 319 |
-
r = catalog.runnable({"env": {"network": "no-network", "image": "python:3.12"}}, {})
|
| 320 |
-
assert not r["ok"] and "internet" in r["why"]
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
def test_indexing_is_capped(monkeypatch):
|
| 324 |
-
monkeypatch.setattr(catalog, "info", lambda spec, token=None: {"sha": "s1", "restricted": False})
|
| 325 |
-
monkeypatch.setattr(catalog, "_read_index", lambda spec, sha=None: None)
|
| 326 |
-
monkeypatch.setattr(catalog, "_build", lambda *a, **k: None)
|
| 327 |
-
with catalog._jobs_lock:
|
| 328 |
-
saved = dict(catalog._jobs)
|
| 329 |
-
catalog._jobs.clear()
|
| 330 |
-
catalog._jobs.update({f"o/busy{i}": {"state": "reading", "sha": "x"} for i in range(config.MAX_INDEX_JOBS)})
|
| 331 |
-
try:
|
| 332 |
-
with pytest.raises(RuntimeError):
|
| 333 |
-
catalog.index_status("o/new-one")
|
| 334 |
-
finally:
|
| 335 |
-
with catalog._jobs_lock:
|
| 336 |
-
catalog._jobs.clear()
|
| 337 |
-
catalog._jobs.update(saved)
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
def test_huge_tasks_are_refused(monkeypatch):
|
| 341 |
-
runnable_task(monkeypatch, bytes=50 * 1024**3)
|
| 342 |
-
r = client.post("/api/runs", json={"dataset": "org/ds", "path": "tasks/t1", "model": "m/x"}, headers=cookie("alice"))
|
| 343 |
-
assert r.status_code == 400 and "GB" in r.json()["detail"]
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
# ── the run list agrees with the run files ───────────────────────────────────
|
| 347 |
-
def test_list_follows_records_written_elsewhere():
|
| 348 |
-
r = run(status="running", reward=None)
|
| 349 |
-
store.list_runs() # index it
|
| 350 |
-
doc = json.loads((store.RUNS / r["id"] / "run.json").read_text())
|
| 351 |
-
time.sleep(0.01)
|
| 352 |
-
doc.update(status="done", reward=1.0, updated_at=time.time())
|
| 353 |
-
(store.RUNS / r["id"] / "run.json").write_text(json.dumps(doc)) # another process finishes it
|
| 354 |
-
store._synced_at = 0
|
| 355 |
-
assert next(x for x in store.list_runs() if x["id"] == r["id"])["status"] == "done"
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
def test_only_silent_rollouts_are_marked_interrupted():
|
| 359 |
-
busy = run(status="running", reward=None) # touched just now: someone is running it
|
| 360 |
-
lost = run(status="running", reward=None)
|
| 361 |
-
doc = json.loads((store.RUNS / lost["id"] / "run.json").read_text())
|
| 362 |
-
doc["updated_at"] = time.time() - 3600 # silent for an hour: its worker is gone
|
| 363 |
-
(store.RUNS / lost["id"] / "run.json").write_text(json.dumps(doc))
|
| 364 |
-
store._synced_at = 0
|
| 365 |
-
store.mark_interrupted(set())
|
| 366 |
-
assert store.get(busy["id"])["status"] == "running"
|
| 367 |
-
assert store.get(lost["id"])["status"] == "interrupted"
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
# ── a task's ${VAR}s never reach this server's environment ───────────────────
|
| 371 |
-
def test_task_variables_never_come_from_this_servers_environment(monkeypatch):
|
| 372 |
-
monkeypatch.setenv("SESSION_SECRET_PROBE", "s3cret-value")
|
| 373 |
-
monkeypatch.setenv("HF_TOKEN", "hf_operator_token")
|
| 374 |
-
with pytest.raises(runner.MissingTaskEnv):
|
| 375 |
-
runner.resolve_task_env({"X": "${SESSION_SECRET_PROBE}"})
|
| 376 |
-
with pytest.raises(runner.MissingTaskEnv):
|
| 377 |
-
runner.resolve_task_env({"KEY": "${HF_TOKEN}"})
|
| 378 |
-
assert runner.resolve_task_env({"A": "${HF_TOKEN:-none}", "B": "literal"}) == {"A": "none", "B": "literal"}
|
| 379 |
-
token = runner._TASK_ENV.set({"environment": {"OPENAI_API_KEY": "cap-123"}, "verifier": {"JUDGE": "cap-456"}})
|
| 380 |
-
try:
|
| 381 |
-
assert runner.resolve_task_env({"K": "${OPENAI_API_KEY}"}) == {"K": "cap-123"}
|
| 382 |
-
with pytest.raises(runner.MissingTaskEnv): # the grader's values never reach the agent's phase
|
| 383 |
-
runner.resolve_task_env({"J": "${JUDGE}"})
|
| 384 |
-
assert runner._resolver("verifier")({"J": "${JUDGE}"}) == {"J": "cap-456"}
|
| 385 |
-
finally:
|
| 386 |
-
runner._TASK_ENV.reset(token)
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
def test_harbor_and_its_agents_are_cut_off_from_this_servers_secrets(monkeypatch):
|
| 390 |
-
monkeypatch.setenv("HF_TOKEN", "hf_operator_token")
|
| 391 |
-
monkeypatch.setenv("OAUTH_CLIENT_SECRET", "oauth-secret")
|
| 392 |
-
monkeypatch.setenv("PATH_LIKE_SETTING", "fine")
|
| 393 |
-
runner._patch_harbor_env()
|
| 394 |
-
import harbor.environments.base as env_base
|
| 395 |
-
import harbor.trial.trial as trial
|
| 396 |
-
import harbor.verifier.verifier as verifier
|
| 397 |
-
from harbor.agents import base as agent_base
|
| 398 |
-
from harbor.utils import env as harbor_env
|
| 399 |
-
for mod, phase in ((env_base, "environment"), (trial, "any"), (verifier, "verifier"), (harbor_env, "environment")):
|
| 400 |
-
assert getattr(mod.resolve_env_vars, "_rlx_phase", None) == phase, mod.__name__
|
| 401 |
-
host = runner._HostEnv()
|
| 402 |
-
assert "HF_TOKEN" not in host and "OAUTH_CLIENT_SECRET" not in host and host.get("PATH_LIKE_SETTING") == "fine"
|
| 403 |
-
fake = type("A", (), {"_extra_env": {}})()
|
| 404 |
-
sources = agent_base.BaseAgent._env_sources(fake)
|
| 405 |
-
assert all("HF_TOKEN" not in s and "OAUTH_CLIENT_SECRET" not in s for s in sources)
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
def test_a_public_rollouts_files_are_images_only_to_others():
|
| 409 |
-
r = run(user="alice", status="done", reward=1.0, visibility="public")
|
| 410 |
-
store.write_artifact(r["id"], "trajectory.json", '[{"role": "user", "content": "from alice"}]')
|
| 411 |
-
store.write_artifact(r["id"], "screenshot.jpg", b"\xff\xd8\xff")
|
| 412 |
-
assert client.get(f"/api/runs/{r['id']}/artifacts/trajectory.json").status_code == 404
|
| 413 |
-
assert client.get(f"/api/runs/{r['id']}/artifacts/screenshot.jpg").status_code == 200
|
| 414 |
-
assert client.get(f"/api/runs/{r['id']}/artifacts/trajectory.json", headers=cookie("alice")).status_code == 200
|
| 415 |
-
assert client.get(f"/api/runs/{r['id']}/artifacts/run.json", headers=cookie("alice")).status_code == 404
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
# ── sign-in ──────────────────────────────────────────────────────────────────
|
| 419 |
-
def _hub_signin(monkeypatch):
|
| 420 |
-
from types import SimpleNamespace
|
| 421 |
-
|
| 422 |
-
monkeypatch.setattr(auth.httpx, "post", lambda *a, **k: SimpleNamespace(status_code=200, json=lambda: {"access_token": "hf_oauth_x", "scope": "openid inference-api jobs", "expires_in": 3600}))
|
| 423 |
-
monkeypatch.setattr(auth.httpx, "get", lambda *a, **k: SimpleNamespace(json=lambda: {"preferred_username": "carol"}))
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
def test_oauth_state_lives_in_a_signed_cookie_for_one_browser(monkeypatch):
|
| 427 |
-
_hub_signin(monkeypatch)
|
| 428 |
-
c = TestClient(main.app, base_url="https://testserver")
|
| 429 |
-
r = c.get("/login", follow_redirects=False)
|
| 430 |
-
assert r.status_code in (302, 307)
|
| 431 |
-
state = dict(x.split("=", 1) for x in r.headers["location"].split("?", 1)[1].split("&"))["state"]
|
| 432 |
-
sent = r.cookies.get(auth.STATE_COOKIE)
|
| 433 |
-
assert sent and state not in sent # encrypted, not the state itself
|
| 434 |
-
other = TestClient(main.app, base_url="https://testserver") # another browser, the same callback URL: refused
|
| 435 |
-
assert other.get(f"/login/callback?code=x&state={state}", follow_redirects=False).status_code == 400
|
| 436 |
-
assert c.get("/login/callback?code=x&state=forged", cookies={auth.STATE_COOKIE: sent}, follow_redirects=False).status_code == 400
|
| 437 |
-
ok = c.get(f"/login/callback?code=x&state={state}", cookies={auth.STATE_COOKIE: sent}, follow_redirects=False)
|
| 438 |
-
assert ok.status_code in (302, 307) and auth.COOKIE in ok.headers.get("set-cookie", "")
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
def test_an_expired_sign_in_is_refused(monkeypatch):
|
| 442 |
-
_hub_signin(monkeypatch)
|
| 443 |
-
old = auth._box.encrypt_at_time(b"s1", int(time.time()) - auth.STATE_TTL - 5).decode()
|
| 444 |
-
r = client.get("/login/callback?code=x&state=s1", cookies={auth.STATE_COOKIE: old}, follow_redirects=False)
|
| 445 |
-
assert r.status_code == 400
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
def test_a_cancelled_sign_in_goes_home():
|
| 449 |
-
r = client.get("/login/callback?error=access_denied", follow_redirects=False)
|
| 450 |
-
assert r.status_code in (302, 307) and r.headers["location"] == "/"
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
def test_pasting_tokens_is_rate_limited(monkeypatch):
|
| 454 |
-
from types import SimpleNamespace
|
| 455 |
-
|
| 456 |
-
monkeypatch.setattr(auth, "TOKEN_LIMIT", auth.Limiter(3, 600))
|
| 457 |
-
monkeypatch.setattr(auth.httpx, "get", lambda *a, **k: SimpleNamespace(status_code=401))
|
| 458 |
-
codes = [client.post("/api/login/token", json={"token": "hf_" + "x" * 30}).status_code for _ in range(5)]
|
| 459 |
-
assert codes == [400, 400, 400, 429, 429]
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
def test_two_submits_at_once_cant_both_take_the_last_place(monkeypatch):
|
| 463 |
-
import threading as th
|
| 464 |
-
|
| 465 |
-
runnable_task(monkeypatch)
|
| 466 |
-
runner._live.clear()
|
| 467 |
-
real_create = store.create
|
| 468 |
-
|
| 469 |
-
def slow_create(doc): # the record takes a moment to write: the window two submits used to race through
|
| 470 |
-
time.sleep(0.3)
|
| 471 |
-
return real_create(doc)
|
| 472 |
-
|
| 473 |
-
monkeypatch.setattr(store, "create", slow_create)
|
| 474 |
-
before = {k: settings.get(k) for k in ("max_per_user", "max_active")}
|
| 475 |
-
settings.save("alice", {"rollouts_enabled": True, "max_per_user": 1, "max_active": 5})
|
| 476 |
-
codes: list[int] = []
|
| 477 |
-
try:
|
| 478 |
-
ts = [th.Thread(target=lambda: codes.append(client.post("/api/runs", json={"dataset": "org/ds", "path": "tasks/t1", "model": "m/x"},
|
| 479 |
-
headers=cookie("bob")).status_code)) for _ in range(3)]
|
| 480 |
-
[t.start() for t in ts]
|
| 481 |
-
[t.join() for t in ts]
|
| 482 |
-
assert sorted(codes) == [200, 429, 429]
|
| 483 |
-
assert runner._reserved == {} # every reservation let go, the refused ones too
|
| 484 |
-
finally:
|
| 485 |
-
runner._live.clear()
|
| 486 |
-
settings.save("alice", {"rollouts_enabled": True, **{k: v for k, v in before.items() if v is not None}})
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
def test_a_live_rollouts_events_stay_local_until_flushed(monkeypatch):
|
| 490 |
-
r = run(user="alice", status="running")
|
| 491 |
-
store.append_events(r["id"], [{"kind": "turn", "n": 1}])
|
| 492 |
-
on_store = (store._dir(r["id"]) / "events.jsonl")
|
| 493 |
-
assert store.read_events(r["id"]) == [{"kind": "turn", "n": 1}] # the page reads the local copy at once
|
| 494 |
-
assert on_store.read_text() == "" # the store (a bucket) wasn't rewritten per event
|
| 495 |
-
monkeypatch.setattr(store, "FLUSH_EVERY", 0.0)
|
| 496 |
-
store.flush_due()
|
| 497 |
-
assert json.loads(on_store.read_text().splitlines()[0]) == {"kind": "turn", "n": 1}
|
| 498 |
-
store.append_events(r["id"], [{"kind": "turn", "n": 2}])
|
| 499 |
-
store.update(r["id"], status="done") # it ended: everything is on the store, no local copy
|
| 500 |
-
assert len(on_store.read_text().splitlines()) == 2 and not (store.LOCAL / r["id"]).exists()
|
| 501 |
-
assert store.read_events(r["id"], after=1) == [{"kind": "turn", "n": 2}]
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
def test_events_a_stopped_process_left_on_local_disk_are_kept():
|
| 505 |
-
r = run(user="alice", status="running")
|
| 506 |
-
store.append_events(r["id"], [{"kind": "turn", "n": 1}])
|
| 507 |
-
store._local.pop(r["id"]) # the process stopped: nothing in memory, the local file left behind
|
| 508 |
-
store._write_json(store._dir(r["id"]) / "run.json", {**store.get(r["id"]), "updated_at": time.time() - store.STALE - 10})
|
| 509 |
-
store._index_put(store.get(r["id"]))
|
| 510 |
-
assert store.mark_interrupted(set()) >= 1
|
| 511 |
-
assert store.get(r["id"])["status"] == "interrupted"
|
| 512 |
-
assert store.read_events(r["id"]) == [{"kind": "turn", "n": 1}] and not (store.LOCAL / r["id"]).exists()
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
def test_a_visitors_endpoint_cant_rebind_to_a_private_address_later(monkeypatch):
|
| 516 |
-
import socket
|
| 517 |
-
|
| 518 |
-
answers = {"model.example": "93.184.216.34", "internal.example": "10.0.0.5"}
|
| 519 |
-
|
| 520 |
-
def fake(host, port, *a, **k):
|
| 521 |
-
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (answers[host], port))]
|
| 522 |
-
|
| 523 |
-
monkeypatch.setattr(endpoints, "_real_getaddrinfo", fake)
|
| 524 |
-
real = socket.getaddrinfo
|
| 525 |
-
try:
|
| 526 |
-
assert endpoints.check_url("https://model.example/v1") == "https://model.example/v1"
|
| 527 |
-
assert socket.getaddrinfo("model.example", 443)[0][4][0] == "93.184.216.34"
|
| 528 |
-
answers["model.example"] = "169.254.169.254" # the name now points at the metadata service
|
| 529 |
-
with pytest.raises(socket.gaierror):
|
| 530 |
-
socket.getaddrinfo("model.example", 443) # any client in this process (the capture proxy's too)
|
| 531 |
-
assert socket.getaddrinfo("internal.example", 443)[0][4][0] == "10.0.0.5" # names nobody gave us: untouched
|
| 532 |
-
finally:
|
| 533 |
-
socket.getaddrinfo = real
|
| 534 |
-
endpoints._untrusted.discard("model.example")
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
def test_a_rollout_never_downloads_reference_solutions(monkeypatch):
|
| 538 |
-
import huggingface_hub
|
| 539 |
-
|
| 540 |
-
seen = {}
|
| 541 |
-
monkeypatch.setattr(catalog, "info", lambda spec, token=None: {"sha": "abc", "restricted": False})
|
| 542 |
-
monkeypatch.setattr(huggingface_hub, "snapshot_download", lambda *a, **k: seen.update(k))
|
| 543 |
-
catalog.task_dir("org/ds", "tasks/t1")
|
| 544 |
-
from fnmatch import fnmatch
|
| 545 |
-
for f in ("tasks/t1/solution/solve.sh", "tasks/t1/steps/s1/solution/solve.sh", "tasks/t1/steps/two/solution/deep/x.py"):
|
| 546 |
-
assert any(fnmatch(f, p) for p in seen["ignore_patterns"]), f
|
| 547 |
-
assert not any(fnmatch("tasks/t1/steps/s1/instruction.md", p) for p in seen["ignore_patterns"])
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
def test_the_admin_space_has_health_checks_and_nothing_else_open():
|
| 551 |
-
assert admin.get("/healthz").json() == {"ok": True}
|
| 552 |
-
r = admin.get("/readyz")
|
| 553 |
-
assert r.status_code == 200 and r.json()["store"] == "ok"
|
| 554 |
-
assert admin.get("/api/admin/settings").status_code == 401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_seo.py
DELETED
|
@@ -1,118 +0,0 @@
|
|
| 1 |
-
"""Every page at its own address, findable and previewable (app/seo.py), without starting work or leaking anything
|
| 2 |
-
(no network: the catalog's listing is stubbed).
|
| 3 |
-
|
| 4 |
-
What is checked: each page answers with its own title, description, canonical address and schema.org data; what a
|
| 5 |
-
page shows comes escaped (a task title can't inject markup or end the JSON-LD script); pages that are personal or
|
| 6 |
-
unknown aren't indexed; old addresses move for good; robots.txt keeps crawlers off the API and rollouts; the
|
| 7 |
-
sitemap lists environments and indexed tasks; preview images are images; unknown paths are 404 pages.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import json
|
| 13 |
-
import re
|
| 14 |
-
|
| 15 |
-
import pytest
|
| 16 |
-
from fastapi.testclient import TestClient
|
| 17 |
-
|
| 18 |
-
from app import main, seo, snapshot
|
| 19 |
-
|
| 20 |
-
client = TestClient(main.app, base_url="https://testserver")
|
| 21 |
-
EVIL = '</script><script>alert(1)</script>"<b>'
|
| 22 |
-
ROWS = [{"id": "org/ds", "key": "org/ds", "kind": "dataset", "heading": f"Bench {EVIL}", "brief": "A bench.", "tags": ["code"], "framework": "harbor",
|
| 23 |
-
"indexed": {"tasks": 2}, "updated": "2026-10-01T00:00:00+00:00", "downloads": 10, "likes": 2, "trending": 1},
|
| 24 |
-
{"id": "org/sp", "key": "space:org/sp", "kind": "space", "heading": "Game", "brief": "A game env.", "tags": ["openenv"], "trending": 0,
|
| 25 |
-
"framework": "openenv", "likes": 3},
|
| 26 |
-
{"id": "org/copy", "key": "space:org/copy", "kind": "space", "heading": "Echo copy", "tags": ["openenv"], "framework": "openenv", "likes": 0}]
|
| 27 |
-
TASKS = [{"path": "tasks/a", "title": f"Fix it {EVIL}", "brief": "Do it.", "category": "code"}, {"path": "tasks/b", "title": "B", "brief": "", "category": None}]
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
@pytest.fixture(autouse=True)
|
| 31 |
-
def stubs(monkeypatch):
|
| 32 |
-
snapshot.reset() # seo reads the listing through the snapshot (a live one here): built from these rows
|
| 33 |
-
seo._listing.update(at=0.0, by={}, rows=[])
|
| 34 |
-
seo._sitemap.update(at=0.0, pages=[], tasks=[])
|
| 35 |
-
monkeypatch.setattr(seo.catalog, "environments", lambda include_hidden=False: ROWS)
|
| 36 |
-
monkeypatch.setattr(seo, "index_rows", lambda spec: TASKS if spec == "org/ds" else [])
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def head(html: str) -> dict:
|
| 40 |
-
return {"title": re.search(r"<title>(.*?)</title>", html).group(1),
|
| 41 |
-
"description": re.search(r'<meta name="description" content="([^"]*)"', html).group(1),
|
| 42 |
-
"canonical": re.search(r'rel="canonical" href="([^"]*)"', html).group(1),
|
| 43 |
-
"robots": re.search(r'name="robots" content="([^"]*)"', html).group(1),
|
| 44 |
-
"ld": [json.loads(x) for x in re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.S)]}
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def test_every_page_has_its_own_head():
|
| 48 |
-
for path, kind in [("/", "WebSite"), ("/d/org/ds", "Dataset"), ("/t/org/ds/tasks/a", "CreativeWork"), ("/s/org/sp", "SoftwareApplication")]:
|
| 49 |
-
r = client.get(path)
|
| 50 |
-
assert r.status_code == 200 and r.headers["content-type"].startswith("text/html")
|
| 51 |
-
h = head(r.text)
|
| 52 |
-
assert h["canonical"] == "https://testserver" + path and "index" in h["robots"] and not h["robots"].startswith("noindex")
|
| 53 |
-
assert kind in [x["@type"] for x in h["ld"]] and h["description"]
|
| 54 |
-
assert "Bench" in head(client.get("/d/org/ds").text)["title"]
|
| 55 |
-
assert client.get("/t/org/ds/tasks/a").text.count('href="/t/') == 0 # a task page links up, not sideways
|
| 56 |
-
assert client.get("/d/org/ds").text.count('href="/t/org/ds/tasks/') == 2 # crawlable links to its tasks
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def test_what_a_page_shows_is_escaped():
|
| 60 |
-
for path in ("/d/org/ds", "/t/org/ds/tasks/a", "/"):
|
| 61 |
-
html = client.get(path).text
|
| 62 |
-
lds = re.findall(r'<script type="application/ld\+json">(.*?)</script>', html, re.S)
|
| 63 |
-
for x in lds:
|
| 64 |
-
assert "</" not in x # nothing in the data can end its script early
|
| 65 |
-
json.loads(x)
|
| 66 |
-
rest = re.sub(r'<script type="application/ld\+json">.*?</script>', "", html, flags=re.S)
|
| 67 |
-
assert "<script>alert(1)" not in rest and '"<b>' not in rest, path
|
| 68 |
-
assert "</script>" in rest or path == "/" # shown, as text
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
def test_personal_and_unknown_pages_arent_indexed():
|
| 72 |
-
for path in ("/runs", "/run/20260101-000000-abcdef", "/compare/org/ds/tasks/a?r=x,y", "/d/someone/unknown", "/t/org/ds/tasks/zzz"):
|
| 73 |
-
assert head(client.get(path).text)["robots"].startswith("noindex"), path
|
| 74 |
-
r = client.get("/nowhere/at/all")
|
| 75 |
-
assert r.status_code == 404 and head(r.text)["robots"].startswith("noindex")
|
| 76 |
-
assert client.get("/api/nothing").json() == {"detail": "Not Found"}
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
def test_old_addresses_move_for_good():
|
| 80 |
-
for old, new in [("/r/org/ds?c=default&s=train&i=3", "/t/org/ds/train/3"), ("/r/org/ds?c=code&s=train&i=6", "/t/org/ds/code/train/6"),
|
| 81 |
-
("/task/music-1", f"/t/{seo.MIMO}/music-1"), ("/rewards", f"/d/{seo.MIMO}?rewards=1")]:
|
| 82 |
-
r = client.get(old, follow_redirects=False)
|
| 83 |
-
assert r.status_code == 301 and r.headers["location"] == new, old
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def test_robots_and_sitemap():
|
| 87 |
-
t = client.get("/robots.txt").text
|
| 88 |
-
for line in ("Disallow: /api/", "Disallow: /mcp/", "Disallow: /run/", "Sitemap: https://testserver/sitemap.xml"):
|
| 89 |
-
assert line in t
|
| 90 |
-
idx = client.get("/sitemap.xml").text
|
| 91 |
-
assert "<sitemapindex" in idx and "/sitemap-pages.xml" in idx and "/sitemap-tasks-1.xml" in idx
|
| 92 |
-
pages = client.get("/sitemap-pages.xml").text
|
| 93 |
-
assert "https://testserver/d/org/ds" in pages and "https://testserver/s/org/sp" in pages and "<lastmod>2026-10-01</lastmod>" in pages
|
| 94 |
-
assert "https://testserver/s/org/copy" not in pages # an unliked, sleeping copy isn't put forward
|
| 95 |
-
tasks = client.get("/sitemap-tasks-1.xml").text
|
| 96 |
-
assert "https://testserver/t/org/ds/tasks/a" in tasks
|
| 97 |
-
assert client.get("/sitemap-tasks-99.xml").status_code == 404
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
def test_preview_images_are_images():
|
| 101 |
-
for path in ("/og.png", "/og/d/org/ds.png", "/og/t/org/ds/tasks/a.png", "/og/s/org/sp.png"):
|
| 102 |
-
r = client.get(path)
|
| 103 |
-
assert r.status_code == 200 and r.headers["content-type"] == "image/png" and r.content[:8] == b"\x89PNG\r\n\x1a\n", path
|
| 104 |
-
assert client.get("/og/d/org/ds.jpg").status_code == 404
|
| 105 |
-
# nothing listed there: no image drawn (and its page points at the site's card instead)
|
| 106 |
-
for path in ("/og/d/nobody/nothing.png", "/og/s/nobody/nothing.png", "/og/whatever.png", "/og/x/y/z.png"):
|
| 107 |
-
assert client.get(path).status_code == 404, path
|
| 108 |
-
assert 'content="https://testserver/og.png"' in client.get("/d/nobody/nothing").text
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
def test_a_card_section_heading_is_not_a_datasets_name():
|
| 112 |
-
from app.catalog import _card_text
|
| 113 |
-
|
| 114 |
-
desc = "NVIDIA NeMo Gym knowledge MCQA dataset for training models on multiple choice questions across domains."
|
| 115 |
-
assert _card_text(f"Dataset Description:\n\n{desc}") == (None, desc)
|
| 116 |
-
assert _card_text(f"Overview\n\n{desc}")[0] is None
|
| 117 |
-
assert _card_text(f"Dataset Card for Terminal Bench\n\n{desc}")[0] == "Terminal Bench"
|
| 118 |
-
assert _card_text(f"Terminal-Bench 2.1\n\n{desc}")[0] == "Terminal-Bench 2.1"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_snapshot.py
DELETED
|
@@ -1,456 +0,0 @@
|
|
| 1 |
-
"""Catalog snapshots (app/snapshot.py) and the indexer that publishes them (app/indexer.py), with no network.
|
| 2 |
-
|
| 3 |
-
What is checked: a snapshot built from listing rows holds only public environments and their tasks' public fields
|
| 4 |
-
(no private or gated dataset, no index built for a signed-in visitor, no metadata values); publishing writes the
|
| 5 |
-
database first and the pointer last, and the app swaps to it while a query on the old one finishes; a pointer whose
|
| 6 |
-
database is corrupt, truncated or not the file it names is rejected and the app keeps what it had (or falls back to
|
| 7 |
-
the live catalog); a newer schema's pointer is left alone; the indexer rebuilds only indexes whose dataset changed,
|
| 8 |
-
within its budget, doesn't retry a failure at the same revision, refuses a listing that shrank, publishes nothing
|
| 9 |
-
when any step fails, reruns safely and prunes old snapshots; the same works through the bucket API.
|
| 10 |
-
"""
|
| 11 |
-
|
| 12 |
-
from __future__ import annotations
|
| 13 |
-
|
| 14 |
-
import gzip
|
| 15 |
-
import json
|
| 16 |
-
import shutil
|
| 17 |
-
import threading
|
| 18 |
-
import time
|
| 19 |
-
from pathlib import Path
|
| 20 |
-
from types import SimpleNamespace
|
| 21 |
-
|
| 22 |
-
import pytest
|
| 23 |
-
|
| 24 |
-
from app import catalog, config, indexer, snapshot
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
def ds(i: int, **kw) -> dict:
|
| 28 |
-
return {"id": f"snaptest/d{i}", "key": f"snaptest/d{i}", "kind": "dataset", "framework": "harbor", "heading": f"D{i}",
|
| 29 |
-
"brief": "tasks", "tags": ["t"], "badges": [], "downloads": i, "likes": 0, "trending": i,
|
| 30 |
-
"updated": "2026-09-01T00:00:00+00:00", **kw}
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def sp(i: int, **kw) -> dict:
|
| 34 |
-
return {"id": f"snaptest/s{i}", "key": f"space:snaptest/s{i}", "kind": "space", "framework": "openenv", "openenv": True,
|
| 35 |
-
"heading": None, "brief": "", "tags": [], "badges": ["OpenEnv"], "downloads": 0, "likes": i, "trending": 0, **kw}
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
ROWS = [ds(0), ds(1), ds(2, private=True), ds(3, gated=True), ds(4), sp(0), sp(1), sp(2)]
|
| 39 |
-
SECRET = "SECRET-ANSWER-4242"
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
def task(path: str, title: str = "A task") -> dict:
|
| 43 |
-
return {"path": path, "title": title, "brief": "Do the thing.", "category": "c", "difficulty": "easy", "tags": ["x"],
|
| 44 |
-
"meta": {"gold_answer": SECRET, "note": SECRET}, "withheld": ["gold_answer"], "verifier": {"kind": "txt", "env": ["OPENAI_KEY"]},
|
| 45 |
-
"env": {"image": "img", "gpus": 0}, "run": "image"}
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def write_index(spec: str, tasks: list[dict], *, restricted: bool = False, version: int | None = None, sha: str = "sha1",
|
| 49 |
-
built: float = 1e9, partial: bool = False) -> None:
|
| 50 |
-
idx = {"spec": spec, "sha": sha, "built": built, "version": version or catalog.INDEX_VERSION, "info": {"restricted": restricted},
|
| 51 |
-
"tasks": tasks, "summary": {}, **({"partial": True} if partial else {})}
|
| 52 |
-
catalog.atomic_write(catalog._index_path(spec), gzip.compress(json.dumps(idx).encode()))
|
| 53 |
-
head = {"tasks": len(tasks), "graded": {"txt": len(tasks)}, "image": len(tasks), "built": built, "sha": sha,
|
| 54 |
-
"version": version or catalog.INDEX_VERSION}
|
| 55 |
-
catalog.atomic_write(catalog._head_path(spec), json.dumps(head).encode())
|
| 56 |
-
catalog._heads.pop(spec, None)
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
@pytest.fixture(autouse=True)
|
| 60 |
-
def isolated(monkeypatch, tmp_path):
|
| 61 |
-
monkeypatch.setenv("RLX_SNAPSHOT_LOCAL", str(tmp_path / "local"))
|
| 62 |
-
monkeypatch.setenv("RLX_SNAPSHOT_STORE", str(tmp_path / "store"))
|
| 63 |
-
monkeypatch.setattr(catalog, "environments", lambda include_hidden=False: ROWS)
|
| 64 |
-
monkeypatch.setattr(catalog, "hidden", lambda: set())
|
| 65 |
-
write_index("snaptest/d0", [task("tasks/a", "Alpha"), task("tasks/b", "Beta")])
|
| 66 |
-
write_index("snaptest/d2", [task("tasks/p")]) # a private dataset's index (a visitor built it)
|
| 67 |
-
write_index("snaptest/d4", [task("tasks/r")], restricted=True) # an index built with a visitor's token
|
| 68 |
-
snapshot.reset()
|
| 69 |
-
listing = dict(catalog._env_list)
|
| 70 |
-
had_listing = (config.STORAGE_DIR / "listing.json.gz").exists()
|
| 71 |
-
yield
|
| 72 |
-
snapshot.reset()
|
| 73 |
-
for p in list(config.INDEX_DIR.glob("snaptest__*")):
|
| 74 |
-
p.unlink(missing_ok=True)
|
| 75 |
-
(config.STORAGE_DIR / indexer.STATE).unlink(missing_ok=True)
|
| 76 |
-
catalog._env_list.clear()
|
| 77 |
-
catalog._env_list.update(listing) # the indexer sets catalog's listing: other tests mustn't see these rows
|
| 78 |
-
if not had_listing:
|
| 79 |
-
(config.STORAGE_DIR / "listing.json.gz").unlink(missing_ok=True)
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def store_of(tmp_path: Path) -> snapshot.LocalStore:
|
| 83 |
-
return snapshot.LocalStore(tmp_path / "store")
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def build(tmp_path: Path, rows=ROWS, name: str = "build.db") -> tuple[Path, dict]:
|
| 87 |
-
db = tmp_path / name
|
| 88 |
-
counts = indexer.build_snapshot(rows, db, {"test": True})
|
| 89 |
-
return db, counts
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
# ── what a snapshot holds ────────────────────────────────────────────────────
|
| 93 |
-
def test_snapshot_holds_public_rows_and_public_fields_only(tmp_path):
|
| 94 |
-
db, counts = build(tmp_path)
|
| 95 |
-
assert counts["envs"] == 6 and counts["datasets"] == 3 and counts["spaces"] == 3
|
| 96 |
-
assert counts["tasks"] == 2 and counts["task_envs"] == 1 # d0's; not the private one's, nor the restricted index's
|
| 97 |
-
raw = db.read_bytes()
|
| 98 |
-
assert SECRET.encode() not in raw and b"gold_answer" not in raw and b"OPENAI_KEY" not in raw
|
| 99 |
-
import sqlite3
|
| 100 |
-
|
| 101 |
-
c = sqlite3.connect(db)
|
| 102 |
-
assert c.execute("PRAGMA journal_mode").fetchone()[0] != "wal"
|
| 103 |
-
assert {r[0] for r in c.execute("SELECT key FROM envs")} == {"snaptest/d0", "snaptest/d1", "snaptest/d4", "space:snaptest/s0",
|
| 104 |
-
"space:snaptest/s1", "space:snaptest/s2"}
|
| 105 |
-
assert c.execute("SELECT tasks, framework FROM envs WHERE key = 'snaptest/d0'").fetchone() == (2, "harbor")
|
| 106 |
-
assert json.loads(c.execute("SELECT v FROM meta WHERE k = 'schema'").fetchone()[0]) == snapshot.SCHEMA
|
| 107 |
-
names = {r[0] for r in c.execute("SELECT name FROM sqlite_master WHERE type = 'index'")}
|
| 108 |
-
assert {"envs_trending", "envs_downloads", "envs_likes", "envs_tasks", "envs_updated", "envs_created", "tasks_env"} <= names
|
| 109 |
-
assert c.execute("SELECT COUNT(*) FROM sqlite_stat1").fetchone()[0] > 0 # ANALYZE ran
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
def test_harbor_whenever_an_index_found_tasks(tmp_path):
|
| 113 |
-
rows = [ds(0, framework="verifiers"), ds(1, framework="verifiers")]
|
| 114 |
-
db, _ = build(tmp_path, rows)
|
| 115 |
-
import sqlite3
|
| 116 |
-
|
| 117 |
-
got = dict(sqlite3.connect(db).execute("SELECT id, framework FROM envs"))
|
| 118 |
-
assert got == {"snaptest/d0": "harbor", "snaptest/d1": "verifiers"}
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
# ── publishing and swapping ──────────────────────────────────────────────────
|
| 122 |
-
class Recording(snapshot.LocalStore):
|
| 123 |
-
def __init__(self, root):
|
| 124 |
-
super().__init__(root)
|
| 125 |
-
self.ops: list[tuple[str, str]] = []
|
| 126 |
-
|
| 127 |
-
def put_file(self, src, rel):
|
| 128 |
-
self.ops.append(("file", rel))
|
| 129 |
-
super().put_file(src, rel)
|
| 130 |
-
|
| 131 |
-
def put_bytes(self, rel, data):
|
| 132 |
-
self.ops.append(("bytes", rel))
|
| 133 |
-
super().put_bytes(rel, data)
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
def test_database_first_pointer_last(tmp_path):
|
| 137 |
-
store = Recording(tmp_path / "store")
|
| 138 |
-
db, counts = build(tmp_path)
|
| 139 |
-
ptr = indexer.publish(store, db, counts)
|
| 140 |
-
assert store.ops == [("file", ptr["db"]), ("bytes", snapshot.POINTER)]
|
| 141 |
-
assert snapshot.read_pointer(store) == ptr and ptr["sha256"] == snapshot.sha256_file(tmp_path / "store" / ptr["db"])
|
| 142 |
-
assert ptr["counts"]["envs"] == 6 and ptr["schema"] == snapshot.SCHEMA
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
def test_app_swaps_to_a_new_snapshot_while_a_query_finishes(tmp_path):
|
| 146 |
-
store = store_of(tmp_path)
|
| 147 |
-
db, counts = build(tmp_path)
|
| 148 |
-
first = indexer.publish(store, db, counts)
|
| 149 |
-
assert snapshot.check(store) and snapshot.current().name == first["db"]
|
| 150 |
-
with snapshot.use() as (old, conn):
|
| 151 |
-
assert conn.execute("SELECT COUNT(*) FROM envs").fetchone()[0] == 6
|
| 152 |
-
db2, counts2 = build(tmp_path, ROWS + [ds(9)], "build2.db")
|
| 153 |
-
second = indexer.publish(store, db2, counts2, now=snapshot.datetime(2030, 1, 1, tzinfo=snapshot.UTC))
|
| 154 |
-
assert snapshot.check(store) # swapped under a running query
|
| 155 |
-
assert snapshot.current().name == second["db"] and old.name == first["db"]
|
| 156 |
-
assert conn.execute("SELECT COUNT(*) FROM envs").fetchone()[0] == 6 # the old one still answers
|
| 157 |
-
assert not old._closed
|
| 158 |
-
assert old._closed # closed once its last query was done
|
| 159 |
-
with snapshot.use() as (new, conn):
|
| 160 |
-
assert new.name == second["db"] and conn.execute("SELECT COUNT(*) FROM envs").fetchone()[0] == 7
|
| 161 |
-
assert not snapshot.check(store) # nothing new: no work
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
def test_concurrent_queries_during_swaps(tmp_path):
|
| 165 |
-
store = store_of(tmp_path)
|
| 166 |
-
db, counts = build(tmp_path)
|
| 167 |
-
indexer.publish(store, db, counts)
|
| 168 |
-
snapshot.check(store)
|
| 169 |
-
errors: list[BaseException] = []
|
| 170 |
-
stop = threading.Event()
|
| 171 |
-
|
| 172 |
-
def reader():
|
| 173 |
-
while not stop.is_set():
|
| 174 |
-
try:
|
| 175 |
-
with snapshot.use() as (_, conn):
|
| 176 |
-
assert conn.execute("SELECT COUNT(*) FROM envs").fetchone()[0] in (6, 7)
|
| 177 |
-
except BaseException as e: # noqa: BLE001
|
| 178 |
-
errors.append(e)
|
| 179 |
-
|
| 180 |
-
threads = [threading.Thread(target=reader) for _ in range(6)]
|
| 181 |
-
for t in threads:
|
| 182 |
-
t.start()
|
| 183 |
-
for i in range(6):
|
| 184 |
-
rows = ROWS + ([ds(9)] if i % 2 == 0 else [])
|
| 185 |
-
dbi, ci = build(tmp_path, rows, f"b{i}.db")
|
| 186 |
-
indexer.publish(store, dbi, ci, now=snapshot.datetime(2030, 1, 1, 0, 0, i, tzinfo=snapshot.UTC))
|
| 187 |
-
assert snapshot.check(store)
|
| 188 |
-
stop.set()
|
| 189 |
-
for t in threads:
|
| 190 |
-
t.join()
|
| 191 |
-
assert not errors
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
def test_corrupt_or_mismatched_snapshots_are_rejected(tmp_path):
|
| 195 |
-
store = store_of(tmp_path)
|
| 196 |
-
good_db, counts = build(tmp_path)
|
| 197 |
-
good = indexer.publish(store, good_db, counts)
|
| 198 |
-
assert snapshot.check(store)
|
| 199 |
-
root = tmp_path / "store"
|
| 200 |
-
|
| 201 |
-
def point(name: str, data: bytes, sha: str | None = None, **extra) -> None:
|
| 202 |
-
(root / name).write_bytes(data)
|
| 203 |
-
ptr = {"db": name, "sha256": sha or snapshot.hashlib.sha256(data).hexdigest(), "size": len(data), "built_at": "x",
|
| 204 |
-
"schema": snapshot.SCHEMA, "counts": counts, **extra}
|
| 205 |
-
store.put_bytes(snapshot.POINTER, json.dumps(ptr).encode())
|
| 206 |
-
|
| 207 |
-
raw = good_db.read_bytes()
|
| 208 |
-
point("snapshots/catalog-20300101T000001Z.db", raw, sha="0" * 64) # not the file the pointer names
|
| 209 |
-
assert not snapshot.check(store) and "sha256" in snapshot.status()["error"]
|
| 210 |
-
point("snapshots/catalog-20300101T000002Z.db", b"not a database" * 100) # garbage with a matching sha
|
| 211 |
-
assert not snapshot.check(store)
|
| 212 |
-
point("snapshots/catalog-20300101T000003Z.db", raw[: len(raw) // 2]) # truncated
|
| 213 |
-
assert not snapshot.check(store)
|
| 214 |
-
point("snapshots/catalog-20300101T000004Z.db", raw, counts={**counts, "envs": 99}) # not what the pointer says
|
| 215 |
-
assert not snapshot.check(store)
|
| 216 |
-
store.put_bytes(snapshot.POINTER, b"{not json")
|
| 217 |
-
assert not snapshot.check(store)
|
| 218 |
-
store.put_bytes(snapshot.POINTER, json.dumps({"db": "../../etc/passwd", "sha256": "0" * 64, "schema": 1}).encode())
|
| 219 |
-
assert not snapshot.check(store)
|
| 220 |
-
assert snapshot.current().name == good["db"] # the good one stayed in use
|
| 221 |
-
assert not list((tmp_path / "local").glob("catalog-20300101T00000[234]Z.db")) # rejected files aren't kept
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
def test_no_usable_snapshot_falls_back_to_the_live_catalog(tmp_path):
|
| 225 |
-
store = store_of(tmp_path)
|
| 226 |
-
(tmp_path / "store" / "snapshots").mkdir(parents=True)
|
| 227 |
-
(tmp_path / "store" / "snapshots" / "catalog-20300101T000000Z.db").write_bytes(b"garbage")
|
| 228 |
-
store.put_bytes(snapshot.POINTER, json.dumps({"db": "snapshots/catalog-20300101T000000Z.db", "sha256": "1" * 64,
|
| 229 |
-
"schema": snapshot.SCHEMA, "counts": {}}).encode())
|
| 230 |
-
with snapshot.use() as (snap, conn):
|
| 231 |
-
assert snap.source == "live" and conn.execute("SELECT COUNT(*) FROM envs").fetchone()[0] == 6
|
| 232 |
-
assert conn.execute("SELECT COUNT(*) FROM tasks").fetchone()[0] == 2
|
| 233 |
-
assert snapshot.status()["source"] == "live" and "rejected" in snapshot.status()["error"]
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
def test_a_newer_schemas_pointer_is_not_read(tmp_path):
|
| 237 |
-
store = store_of(tmp_path)
|
| 238 |
-
store.put_bytes(f"snapshots/LATEST.v{snapshot.SCHEMA + 1}.json", b'{"db": "snapshots/catalog-1Z.db"}')
|
| 239 |
-
assert snapshot.read_pointer(store) is None and not snapshot.check(store)
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
# ── the indexer ──────────────────────────────────────────────────────────────
|
| 243 |
-
def test_plan_rebuilds_only_what_changed(tmp_path, monkeypatch):
|
| 244 |
-
monkeypatch.setattr(catalog, "featured_datasets", lambda: ["snaptest/f1"])
|
| 245 |
-
built = 1_790_000_000.0 # 2026-09-21
|
| 246 |
-
write_index("snaptest/same", [task("t")], sha="s1", built=built)
|
| 247 |
-
write_index("snaptest/touched", [task("t")], sha="s1", built=built)
|
| 248 |
-
write_index("snaptest/moved", [task("t")], sha="old", built=built)
|
| 249 |
-
write_index("snaptest/oldfmt", [task("t")], version=catalog.INDEX_VERSION - 1, built=built)
|
| 250 |
-
write_index("snaptest/part", [task("t")], built=built)
|
| 251 |
-
rows = [{**ds(0), "id": i, "key": i, "updated": u, "trending": t} for i, u, t in [
|
| 252 |
-
("snaptest/same", "2026-09-01T00:00:00+00:00", 9), # changed before its index was built: no Hub call
|
| 253 |
-
("snaptest/touched", "2026-09-30T00:00:00+00:00", 8), # changed after, same revision: checked, skipped
|
| 254 |
-
("snaptest/moved", "2026-09-30T00:00:00+00:00", 1), # a new revision
|
| 255 |
-
("snaptest/oldfmt", "2026-09-01T00:00:00+00:00", 7),
|
| 256 |
-
("snaptest/part", "2026-09-01T00:00:00+00:00", 6),
|
| 257 |
-
("snaptest/new", "2026-09-01T00:00:00+00:00", 5),
|
| 258 |
-
("snaptest/f1", "2026-09-01T00:00:00+00:00", 0)]]
|
| 259 |
-
rows.append({**ds(0), "id": "snaptest/rows", "key": "snaptest/rows", "framework": "verl"}) # read row by row: never indexed
|
| 260 |
-
asked: list[str] = []
|
| 261 |
-
|
| 262 |
-
def info(spec):
|
| 263 |
-
asked.append(spec)
|
| 264 |
-
return {"sha": "new" if spec == "snaptest/moved" else "s1"}
|
| 265 |
-
|
| 266 |
-
items = indexer.plan(rows, {"rebuild": ["snaptest/part"]}, info=info)
|
| 267 |
-
assert sorted(asked) == ["snaptest/moved", "snaptest/touched"]
|
| 268 |
-
assert [(i["spec"], i["reason"]) for i in items] == [("snaptest/f1", "new"), ("snaptest/moved", "changed"),
|
| 269 |
-
("snaptest/oldfmt", "format"), ("snaptest/part", "partial"),
|
| 270 |
-
("snaptest/new", "new")]
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
def test_refresh_keeps_to_its_budget_and_doesnt_retry_failures(tmp_path):
|
| 274 |
-
items = [{"spec": f"snaptest/x{i}", "reason": "new", "tagged": True, "featured": False, "trending": 0, "downloads": 0, "head": None}
|
| 275 |
-
for i in range(5)]
|
| 276 |
-
calls: list[str] = []
|
| 277 |
-
|
| 278 |
-
def build(spec, meta):
|
| 279 |
-
calls.append(spec)
|
| 280 |
-
if spec == "snaptest/x1":
|
| 281 |
-
raise ValueError("more than 20,000 tasks")
|
| 282 |
-
return 3
|
| 283 |
-
|
| 284 |
-
state = indexer.load_state()
|
| 285 |
-
info = lambda spec: {"sha": "r1"}
|
| 286 |
-
out = indexer.refresh_indexes(items, state, budget_s=60, max_builds=3, build=build, info=info, looks_harbor=lambda s, h: True)
|
| 287 |
-
assert calls == ["snaptest/x0", "snaptest/x1", "snaptest/x2"] and out["deferred"] == 2
|
| 288 |
-
assert [b["spec"] for b in out["built"]] == ["snaptest/x0", "snaptest/x2"] and out["failed"][0]["spec"] == "snaptest/x1"
|
| 289 |
-
calls.clear()
|
| 290 |
-
out = indexer.refresh_indexes(items[1:2], state, budget_s=60, max_builds=3, build=build, info=info, looks_harbor=lambda s, h: True)
|
| 291 |
-
assert calls == [] and out["skipped"][0]["why"].startswith("failed at this revision")
|
| 292 |
-
out = indexer.refresh_indexes(items[1:2], state, budget_s=60, max_builds=3, build=build, info=lambda s: {"sha": "r2"},
|
| 293 |
-
looks_harbor=lambda s, h: True)
|
| 294 |
-
assert calls == ["snaptest/x1"] # a new revision is tried again
|
| 295 |
-
out = indexer.refresh_indexes(items, state, budget_s=0, max_builds=3, build=build, info=info)
|
| 296 |
-
assert out["deferred"] == 5 # out of time: nothing started
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
def test_indexer_run_publishes_and_prunes(tmp_path, monkeypatch):
|
| 300 |
-
store = Recording(tmp_path / "store")
|
| 301 |
-
monkeypatch.setattr(catalog, "featured_datasets", list)
|
| 302 |
-
monkeypatch.setattr(catalog, "info", lambda spec: {"sha": "zz"})
|
| 303 |
-
builds: list[str] = []
|
| 304 |
-
|
| 305 |
-
def fake_build(spec, meta):
|
| 306 |
-
builds.append(spec)
|
| 307 |
-
write_index(spec, [task("tasks/new")], sha=meta["sha"], built=time.time())
|
| 308 |
-
return 1
|
| 309 |
-
|
| 310 |
-
monkeypatch.setattr(indexer, "_build_one", fake_build)
|
| 311 |
-
reports = []
|
| 312 |
-
for i in range(4):
|
| 313 |
-
reports.append(indexer.run(store, rows=ROWS, budget_s=60, max_builds=10, keep=2))
|
| 314 |
-
time.sleep(1.05) # one snapshot a second at most: names are UTC seconds
|
| 315 |
-
# d4 and d0 changed revision (the Hub says zz), d1 was never indexed; after that, all current: no more builds
|
| 316 |
-
assert builds == ["snaptest/d4", "snaptest/d0", "snaptest/d1"]
|
| 317 |
-
assert [r["indexes"]["built"] for r in reports[1:]] == [[], [], []]
|
| 318 |
-
ptr = snapshot.read_pointer(store)
|
| 319 |
-
assert ptr == reports[-1]["pointer"] and ptr["counts"]["tasks"] == 3
|
| 320 |
-
assert store.ops[-2:] == [("file", ptr["db"]), ("bytes", snapshot.POINTER)]
|
| 321 |
-
dbs = [n for n in store.list("snapshots") if n.endswith(".db")]
|
| 322 |
-
assert len(dbs) == 2 and ptr["db"] in dbs # pruned to --keep
|
| 323 |
-
assert (config.STORAGE_DIR / "listing.json.gz").exists()
|
| 324 |
-
assert snapshot.check(store) and snapshot.current().counts["envs"] == 6
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
def test_cached_listing_publication_preserves_discovery_timestamp(tmp_path):
|
| 328 |
-
at = time.time() - 1800
|
| 329 |
-
indexer.run(snapshot.LocalStore(tmp_path / "store"), rows=ROWS, index=False, listing_at=at)
|
| 330 |
-
data = json.loads(gzip.decompress((config.STORAGE_DIR / "listing.json.gz").read_bytes()))
|
| 331 |
-
assert data["at"] == at
|
| 332 |
-
assert catalog._env_list["at"] == at
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
def test_timed_out_build_backs_off_so_later_batches_can_progress(monkeypatch):
|
| 336 |
-
class Stalled:
|
| 337 |
-
def __init__(self, **kwargs): pass
|
| 338 |
-
def start(self): pass
|
| 339 |
-
def join(self, **kwargs): pass
|
| 340 |
-
def is_alive(self): return True
|
| 341 |
-
monkeypatch.setattr(indexer.threading, "Thread", Stalled)
|
| 342 |
-
items = [{"spec": "snaptest/large", "reason": "new", "tagged": True}]
|
| 343 |
-
state = {"failed": {}, "not_harbor": {}, "rebuild": []}
|
| 344 |
-
info = lambda spec: {"sha": "same-revision"}
|
| 345 |
-
first = indexer.refresh_indexes(items, state, budget_s=90, max_builds=4, info=info)
|
| 346 |
-
assert first["abandoned"] == ["snaptest/large"]
|
| 347 |
-
second = indexer.refresh_indexes(items, state, budget_s=90, max_builds=4, info=info)
|
| 348 |
-
assert second["abandoned"] == [] and second["skipped"][0]["spec"] == "snaptest/large"
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
def test_a_failed_run_publishes_nothing(tmp_path, monkeypatch):
|
| 352 |
-
store = store_of(tmp_path)
|
| 353 |
-
first = indexer.run(store, rows=ROWS, index=False)["pointer"]
|
| 354 |
-
time.sleep(1.05)
|
| 355 |
-
monkeypatch.setattr(snapshot, "build_db", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("disk full")))
|
| 356 |
-
with pytest.raises(RuntimeError):
|
| 357 |
-
indexer.run(store, rows=ROWS, index=False)
|
| 358 |
-
assert snapshot.read_pointer(store) == first
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
def test_a_listing_that_shrank_is_refused(tmp_path):
|
| 362 |
-
store = store_of(tmp_path)
|
| 363 |
-
first = indexer.run(store, rows=ROWS, index=False)["pointer"]
|
| 364 |
-
with pytest.raises(indexer.IndexerError, match="refusing to publish"):
|
| 365 |
-
indexer.run(store, rows=ROWS[:2], index=False) # the Spaces' listing failed
|
| 366 |
-
assert snapshot.read_pointer(store) == first
|
| 367 |
-
time.sleep(1.05)
|
| 368 |
-
assert indexer.run(store, rows=ROWS[:2], index=False, force=True)["pointer"]["counts"]["envs"] == 2
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
def test_dry_run_writes_nothing(tmp_path):
|
| 372 |
-
store = store_of(tmp_path)
|
| 373 |
-
out = tmp_path / "dry.db"
|
| 374 |
-
report = indexer.run(store, rows=ROWS, dry_run=True, out=out)
|
| 375 |
-
assert report["counts"]["envs"] == 6 and out.exists()
|
| 376 |
-
assert snapshot.read_pointer(store) is None and not (tmp_path / "store").exists()
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
def test_main_exit_status(tmp_path, monkeypatch):
|
| 380 |
-
monkeypatch.setenv("STORAGE_DIR", str(config.STORAGE_DIR)) # main() points it at --store: restored after
|
| 381 |
-
monkeypatch.setattr(indexer, "fetch_listing", lambda attempts=2: (_ for _ in ()).throw(indexer.IndexerError("Hub down")))
|
| 382 |
-
monkeypatch.setattr("app.runtime.logs", lambda: None)
|
| 383 |
-
assert indexer.main(["--store", str(tmp_path / "store"), "--no-index"]) == 1
|
| 384 |
-
assert not (tmp_path / "store" / snapshot.POINTER).exists()
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
# ── through the bucket API ───────────────────────────────────────────────────
|
| 388 |
-
class FakeBucketApi:
|
| 389 |
-
"""The four bucket calls the store uses, over a dict."""
|
| 390 |
-
|
| 391 |
-
def __init__(self):
|
| 392 |
-
self.files: dict[str, bytes] = {}
|
| 393 |
-
self.batches: list[dict] = []
|
| 394 |
-
|
| 395 |
-
def batch_bucket_files(self, bucket_id, *, add=None, delete=None, copy=None, token=None):
|
| 396 |
-
self.batches.append({"add": [p for _, p in add or []], "delete": list(delete or [])})
|
| 397 |
-
for src, path in add or []:
|
| 398 |
-
self.files[path] = src if isinstance(src, bytes) else Path(src).read_bytes()
|
| 399 |
-
for path in delete or []:
|
| 400 |
-
self.files.pop(path, None)
|
| 401 |
-
|
| 402 |
-
def download_bucket_files(self, bucket_id, files, *, raise_on_missing_files=False, token=None):
|
| 403 |
-
for path, dst in files:
|
| 404 |
-
if path not in self.files:
|
| 405 |
-
if raise_on_missing_files:
|
| 406 |
-
raise FileNotFoundError(path)
|
| 407 |
-
continue
|
| 408 |
-
Path(dst).write_bytes(self.files[path])
|
| 409 |
-
|
| 410 |
-
def list_bucket_tree(self, bucket_id, prefix=None, *, recursive=None, token=None):
|
| 411 |
-
return [SimpleNamespace(type="file", path=p, size=len(b)) for p, b in sorted(self.files.items()) if p.startswith(prefix or "")]
|
| 412 |
-
|
| 413 |
-
def get_bucket_paths_info(self, bucket_id, paths, *, token=None):
|
| 414 |
-
return [SimpleNamespace(type="file", path=p, size=len(self.files[p])) for p in paths if p in self.files]
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
def test_the_same_through_the_bucket_api(tmp_path):
|
| 418 |
-
api = FakeBucketApi()
|
| 419 |
-
store = snapshot.BucketStore("FineEnvs/rl-explorer-data", api=api)
|
| 420 |
-
report = indexer.run(store, rows=ROWS, index=False, keep=1)
|
| 421 |
-
ptr = report["pointer"]
|
| 422 |
-
assert [b["add"] for b in api.batches] == [[ptr["db"]], [snapshot.POINTER]] # the database, then the pointer
|
| 423 |
-
assert snapshot.check(store) and snapshot.current().counts["envs"] == 6
|
| 424 |
-
time.sleep(1.05)
|
| 425 |
-
indexer.run(store, rows=ROWS, index=False, keep=1)
|
| 426 |
-
assert api.batches[-1]["delete"] == [ptr["db"]] # pruned through the API too
|
| 427 |
-
assert str(snapshot.open_store("hf://buckets/FineEnvs/rl-explorer-data/sub")).endswith("rl-explorer-data/sub")
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
def teardown_module(module):
|
| 431 |
-
shutil.rmtree(Path(config.STORAGE_DIR) / "snapshots", ignore_errors=True)
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
def test_a_snapshot_whose_file_vanished_is_dropped_and_fetched_again(tmp_path):
|
| 435 |
-
store = store_of(tmp_path)
|
| 436 |
-
db, counts = build(tmp_path)
|
| 437 |
-
indexer.publish(store, db, counts)
|
| 438 |
-
assert snapshot.check(store)
|
| 439 |
-
snap = snapshot.current()
|
| 440 |
-
snap._close() # its pooled connections gone, as after a while idle
|
| 441 |
-
snap._closed = False
|
| 442 |
-
snap.path.unlink() # a disk cleaner took the file
|
| 443 |
-
with snapshot.use() as (s, conn): # this request is still answered: the snapshot is fetched again at once
|
| 444 |
-
assert conn.execute("SELECT COUNT(*) FROM envs").fetchone()[0] == 6
|
| 445 |
-
assert s is not snap and s.name == snap.name and s.path.exists() and snapshot.current() is s
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
def test_fallback_files_of_dead_processes_are_cleaned(tmp_path):
|
| 449 |
-
local = tmp_path / "local"
|
| 450 |
-
local.mkdir(parents=True)
|
| 451 |
-
dead, mine = local / "fallback-999999-1.db", local / f"fallback-{snapshot.os.getpid()}-77.db"
|
| 452 |
-
dead.write_bytes(b"x")
|
| 453 |
-
mine.write_bytes(b"x")
|
| 454 |
-
with snapshot.use() as (snap, _):
|
| 455 |
-
assert snap.source == "live"
|
| 456 |
-
assert not dead.exists() and mine.exists()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_space_checks.py
DELETED
|
@@ -1,135 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import time
|
| 3 |
-
|
| 4 |
-
import pytest
|
| 5 |
-
|
| 6 |
-
from app import space_checks as checks, spaces_live as live
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
def facts():
|
| 10 |
-
api = {"info": {"version": "1.0.0"}, "paths": {
|
| 11 |
-
"/reset": {"post": {}}, "/step": {"post": {}}, "/state": {"get": {}}, "/mcp": {"post": {}}}}
|
| 12 |
-
return [api, {"status": "healthy"}, {"name": "test", "description": "test", "version": "9.9.9"},
|
| 13 |
-
{"action": {}, "observation": {}, "state": {}}, None]
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def test_typed_actions_do_not_require_mcp_but_production_does():
|
| 17 |
-
f = facts()
|
| 18 |
-
assert checks.assess(*f)["status"] == checks.PASS
|
| 19 |
-
f[0]["paths"] = {"/mcp": {"post": {}}}
|
| 20 |
-
assert checks.assess(*f)["status"] == "Checks failed"
|
| 21 |
-
f[-1] = [{"name": "echo"}]
|
| 22 |
-
assert checks.assess(*f)["mode"] == "MCP service"
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
@pytest.mark.parametrize("change", ["schema", "health", "route", "method", "html"])
|
| 26 |
-
def test_failures_cannot_become_api_checked(change):
|
| 27 |
-
f = facts()
|
| 28 |
-
if change == "schema":
|
| 29 |
-
f[3].pop("state")
|
| 30 |
-
elif change == "health":
|
| 31 |
-
f[1]["status"] = "ok"
|
| 32 |
-
elif change == "route":
|
| 33 |
-
f[0]["paths"].pop("/step")
|
| 34 |
-
elif change == "method":
|
| 35 |
-
f[0]["paths"]["/step"] = {"get": {}}
|
| 36 |
-
else:
|
| 37 |
-
f[0] = "<html>Space UI</html>"
|
| 38 |
-
assert checks.assess(*f)["status"] == "Checks failed"
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
@pytest.mark.parametrize("dependency,value,source", [
|
| 42 |
-
("openenv-core==0.2.3", "0.2.3", "Dependency pin"),
|
| 43 |
-
("openenv==0.7.0", "0.7.0", "Dependency pin"),
|
| 44 |
-
("openenv_core[core]>=0.2.0,<0.3", "<0.3,>=0.2.0", "Dependency constraint"),
|
| 45 |
-
("openenv-core==0.2.*", "==0.2.*", "Dependency constraint"),
|
| 46 |
-
("openenv-core", "Unpinned", "Dependency constraint"),
|
| 47 |
-
("openenv-core @ git+https://github.com/meta-pytorch/OpenEnv.git@abc123", "git:abc123", "Source reference"),
|
| 48 |
-
('openenv-core==0.2.3; python_version < "3.12"', "Conditional", "Dependency constraint"),
|
| 49 |
-
])
|
| 50 |
-
def test_declared_versions_keep_constraints_and_source_refs(dependency, value, source):
|
| 51 |
-
v = checks.requirement_version(dependency)
|
| 52 |
-
assert (v["value"], v["source"]) == (value, source)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
def test_version_does_not_use_environment_or_api_version():
|
| 56 |
-
assert checks.parse_version('[project]\nname="env"\nversion="9.9.9"\ndependencies=["fastapi"]', "pyproject.toml") is None
|
| 57 |
-
assert checks.requirement_version("gymnasium==1.0.0") is None
|
| 58 |
-
assert checks.tag_version(["openenv", "openenv-nonsense"])["value"] == "Unknown"
|
| 59 |
-
assert checks.tag_version(["openenv-0.2.3"])["value"] == "0.2.3"
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
def test_lock_and_uv_source_precedence():
|
| 63 |
-
doc = '[[package]]\nname="openenv-core"\nversion="0.7.1.dev0"\nsource={git="https://example.org/env?rev=main#abcdef"}'
|
| 64 |
-
assert checks.parse_version(doc, "uv.lock") == {"value": "git:abcdef", "source": "Lockfile"}
|
| 65 |
-
doc = '[project]\ndependencies=["openenv-core>=0.2"]\n[tool.uv.sources]\nopenenv-core={git="https://example.org/env",rev="v0.2.3"}'
|
| 66 |
-
assert checks.parse_version(doc, "pyproject.toml")["value"] == "git:v0.2.3"
|
| 67 |
-
assert checks.parse_version(doc.replace("openenv-core", "openenv"), "pyproject.toml")["value"] == "git:v0.2.3"
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
def test_expiry_future_dates_and_newer_failures(tmp_path, monkeypatch):
|
| 71 |
-
monkeypatch.setattr(checks.config, "STORAGE_DIR", tmp_path)
|
| 72 |
-
now = time.time()
|
| 73 |
-
passing = {"schema": 1, "id": "owner/env", "checked_at": now - 10, "status": checks.PASS}
|
| 74 |
-
checks.save(passing)
|
| 75 |
-
assert checks.status(checks.inventory()["owner/env"], now) == checks.PASS
|
| 76 |
-
assert checks.status(passing, now + checks.FRESH) == "Check expired"
|
| 77 |
-
assert checks.status(passing, now - 20) == "Check expired"
|
| 78 |
-
checks.save({**passing, "checked_at": now, "status": "Checks failed"})
|
| 79 |
-
checks.save(passing) # late completion cannot overwrite a newer failure
|
| 80 |
-
assert checks.status(checks.inventory()["owner/env"], now) == "Checks failed"
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
def test_sleeping_space_is_not_contacted_or_woken(tmp_path, monkeypatch):
|
| 84 |
-
monkeypatch.setattr(checks.config, "STORAGE_DIR", tmp_path)
|
| 85 |
-
monkeypatch.setattr(live, "record", lambda *a, **kw: {"stage": "SLEEPING"})
|
| 86 |
-
monkeypatch.setattr(live, "SpaceClient", lambda *a, **kw: pytest.fail("contacted sleeping server"))
|
| 87 |
-
r = checks.check("owner/sleeping")
|
| 88 |
-
assert r["status"] == "Not running"
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
def test_network_failure_invalidates_previous_pass(tmp_path, monkeypatch):
|
| 92 |
-
monkeypatch.setattr(checks.config, "STORAGE_DIR", tmp_path)
|
| 93 |
-
checks.save({"schema": 1, "id": "owner/env", "checked_at": time.time() - 10, "status": checks.PASS})
|
| 94 |
-
def offline(*a, **kw):
|
| 95 |
-
raise ConnectionError("offline")
|
| 96 |
-
monkeypatch.setattr(live, "record", offline)
|
| 97 |
-
assert checks.check("owner/env")["status"] == "Check unavailable"
|
| 98 |
-
assert checks.status(checks.inventory()["owner/env"]) != checks.PASS
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def test_concurrent_refresh_skips_locked_queue(tmp_path, monkeypatch):
|
| 102 |
-
fcntl = pytest.importorskip("fcntl")
|
| 103 |
-
monkeypatch.setattr(checks.config, "CACHE_DIR", tmp_path)
|
| 104 |
-
monkeypatch.setattr(checks, "_scan", lambda: pytest.fail("started duplicate queue"))
|
| 105 |
-
with (tmp_path / "openenv-checks.lock").open("a") as lease:
|
| 106 |
-
fcntl.flock(lease, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
| 107 |
-
assert checks.scan() == []
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
def test_automatic_queue_includes_inactive_and_untagged_without_starvation():
|
| 111 |
-
candidates = [(f"org/active{i}", "RUNNING") for i in range(120)] + [(f"org/sleep{i}", "SLEEPING") for i in range(40)]
|
| 112 |
-
candidates += [("FineEnvs/recent", "RUNNING"), ("FineEnvs/new", "RUNNING")]
|
| 113 |
-
now = time.time()
|
| 114 |
-
old = {"FineEnvs/recent": {"stage": "RUNNING", "checked_at": now, "status": checks.PASS, "task_catalog": {}}}
|
| 115 |
-
pending = checks.pending(candidates, old, now)
|
| 116 |
-
assert len(pending) == 128 and pending[0] == "FineEnvs/new"
|
| 117 |
-
assert sum("sleep" in spec for spec in pending) == 32
|
| 118 |
-
assert "FineEnvs/recent" not in pending
|
| 119 |
-
for spec in pending:
|
| 120 |
-
old[spec] = {"checked_at": now, "stage": "SLEEPING", "status": "Not running"}
|
| 121 |
-
next_batch = checks.pending(candidates, old, now)
|
| 122 |
-
assert next_batch and not set(next_batch) & set(pending)
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def test_other_environment_interfaces_need_published_methods_and_fresh_running_checks():
|
| 126 |
-
api = {"info": {"version": "1"}, "paths": {"/seed_session": {"post": {}}, "/verify": {"post": {}}}}
|
| 127 |
-
assert checks.environment_interface(api) == "nemo-gym"
|
| 128 |
-
rec = {"interface": "nemo-gym", "status": "Checks failed", "stage": "RUNNING", "checked_at": time.time()}
|
| 129 |
-
assert checks.browseable(rec) and not checks.verified(rec)
|
| 130 |
-
assert not checks.browseable({**rec, "checked_at": time.time() - checks.FRESH})
|
| 131 |
-
assert not checks.browseable({**rec, "stage": "SLEEPING"})
|
| 132 |
-
assert not checks.browseable({**rec, "status": "Check unavailable"})
|
| 133 |
-
api["paths"]["/verify"] = {"get": {}}
|
| 134 |
-
assert checks.environment_interface(api) is None
|
| 135 |
-
assert checks.environment_interface(facts()[0]) is None # generic reset/step alone is insufficient
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_space_files.py
DELETED
|
@@ -1,449 +0,0 @@
|
|
| 1 |
-
"""An environment Space read as files, and what a live probe leaves behind (no network: the Hub is stubbed).
|
| 2 |
-
|
| 3 |
-
What is checked: openenv.yaml in its variants (canonical, legacy class names, hackathon task lists, the `validation:`
|
| 4 |
-
block, a manifest in a subfolder, broken or explosive YAML); models.py, server/app.py and the environment read with
|
| 5 |
-
`ast`, never imported or run; vendored OpenEnv and build output left out of the tree; the file endpoint serves only
|
| 6 |
-
listed text files (no traversal, no binaries, no answers); private Spaces are refused and no token is ever sent;
|
| 7 |
-
the last good probe is kept and shown while the Space sleeps; `openenv validate`'s six criteria are judged right.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
from __future__ import annotations
|
| 11 |
-
|
| 12 |
-
import json
|
| 13 |
-
import time
|
| 14 |
-
from pathlib import Path
|
| 15 |
-
from types import SimpleNamespace
|
| 16 |
-
|
| 17 |
-
import pytest
|
| 18 |
-
from fastapi import FastAPI
|
| 19 |
-
from fastapi.testclient import TestClient
|
| 20 |
-
|
| 21 |
-
from app import catalog, config
|
| 22 |
-
from app import space_files as sf
|
| 23 |
-
from app import spaces_live as live
|
| 24 |
-
|
| 25 |
-
api = FastAPI()
|
| 26 |
-
api.include_router(sf.router)
|
| 27 |
-
client = TestClient(api)
|
| 28 |
-
|
| 29 |
-
SHA = "a" * 40
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
@pytest.fixture(autouse=True)
|
| 33 |
-
def fresh():
|
| 34 |
-
catalog._memo.clear()
|
| 35 |
-
yield
|
| 36 |
-
catalog._memo.clear()
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def sibling(name, size=100, lfs=False):
|
| 40 |
-
return SimpleNamespace(rfilename=name, size=size, lfs={"sha256": "x"} if lfs else None)
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def stub_space(monkeypatch, tmp_path, files: dict[str, str | bytes], private=False, extra: list[str] = (), seen_tokens=None):
|
| 44 |
-
"""A public Space whose repository holds `files` (path -> content) and `extra` (listed only)."""
|
| 45 |
-
sibs = [sibling(p, len(c.encode() if isinstance(c, str) else c)) for p, c in files.items()] + [sibling(p) for p in extra]
|
| 46 |
-
sp = SimpleNamespace(sha=SHA, private=private, gated=False, siblings=sibs, subdomain="org-env", sdk="docker", tags=["openenv"],
|
| 47 |
-
card_data=SimpleNamespace(to_dict=lambda: {"title": "Env", "base_path": "/web", "app_port": 8000}),
|
| 48 |
-
runtime=SimpleNamespace(stage="SLEEPING", hardware=None, requested_hardware="cpu-basic"),
|
| 49 |
-
last_modified=None)
|
| 50 |
-
|
| 51 |
-
def space_info(spec, files_metadata=False, expand=None):
|
| 52 |
-
return sp
|
| 53 |
-
|
| 54 |
-
def _api(token=None):
|
| 55 |
-
if seen_tokens is not None:
|
| 56 |
-
seen_tokens.append(("api", token))
|
| 57 |
-
return SimpleNamespace(space_info=space_info)
|
| 58 |
-
|
| 59 |
-
calls = []
|
| 60 |
-
|
| 61 |
-
def download(repo_id, filename, repo_type=None, revision=None, token=None, cache_dir=None, **kw):
|
| 62 |
-
calls.append({"repo": repo_id, "file": filename, "type": repo_type, "revision": revision, "token": token})
|
| 63 |
-
p = tmp_path / "dl" / filename
|
| 64 |
-
p.parent.mkdir(parents=True, exist_ok=True)
|
| 65 |
-
data = files[filename]
|
| 66 |
-
p.write_bytes(data.encode() if isinstance(data, str) else data)
|
| 67 |
-
return str(p)
|
| 68 |
-
|
| 69 |
-
import huggingface_hub
|
| 70 |
-
|
| 71 |
-
monkeypatch.setattr(catalog, "_api", _api)
|
| 72 |
-
monkeypatch.setattr(huggingface_hub, "hf_hub_download", download)
|
| 73 |
-
return calls
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
# ── openenv.yaml ─────────────────────────────────────────────────────────────
|
| 77 |
-
CANONICAL = """spec_version: 1
|
| 78 |
-
name: echo_env
|
| 79 |
-
type: space
|
| 80 |
-
runtime: fastapi
|
| 81 |
-
app: server.app:app
|
| 82 |
-
port: 8000
|
| 83 |
-
"""
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
def test_canonical_manifest_keys():
|
| 87 |
-
m = sf.parse_manifest(CANONICAL, "openenv.yaml")
|
| 88 |
-
assert m["keys"] == {"spec_version": 1, "name": "echo_env", "type": "space", "runtime": "fastapi", "app": "server.app:app", "port": 8000}
|
| 89 |
-
assert "validation" not in m and "tasks" not in m and "error" not in m
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
def test_legacy_manifest_names_its_classes():
|
| 93 |
-
m = sf.parse_manifest('name: browsergym_env\nversion: "0.1.0"\naction: BrowserGymAction\nobservation: BrowserGymObservation\n', "openenv.yaml")
|
| 94 |
-
assert m["legacy"] == {"action": "BrowserGymAction", "observation": "BrowserGymObservation"}
|
| 95 |
-
assert m["keys"]["version"] == "0.1.0"
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def test_hackathon_task_lists_in_every_shape_and_without_answers():
|
| 99 |
-
listed = sf.parse_manifest("""name: triage
|
| 100 |
-
tasks:
|
| 101 |
-
- id: easy
|
| 102 |
-
name: Direct injection
|
| 103 |
-
difficulty: easy
|
| 104 |
-
description: Detect it.
|
| 105 |
-
expected_score_range: [0.2, 0.5]
|
| 106 |
-
gold_answer: "yes"
|
| 107 |
-
- id: hard
|
| 108 |
-
level: hard
|
| 109 |
-
reward:
|
| 110 |
-
min: 0.0
|
| 111 |
-
max: 1.0
|
| 112 |
-
expected_output: 42
|
| 113 |
-
""", "openenv.yaml")
|
| 114 |
-
assert [t["id"] for t in listed["tasks"]] == ["easy", "hard"]
|
| 115 |
-
assert listed["tasks"][0]["difficulty"] == "easy" and listed["tasks"][1]["difficulty"] == "hard"
|
| 116 |
-
flat = json.dumps(listed)
|
| 117 |
-
assert "gold_answer" not in flat and "expected_score_range" not in flat and "42" not in flat
|
| 118 |
-
assert "expected_output" in listed["withheld"]
|
| 119 |
-
strings = sf.parse_manifest("name: x\ntasks: [easy, medium, hard]\n", "openenv.yaml")
|
| 120 |
-
assert [t["id"] for t in strings["tasks"]] == ["easy", "medium", "hard"]
|
| 121 |
-
keyed = sf.parse_manifest("name: x\ntasks:\n easy: {description: one}\n hard: two\n", "openenv.yaml")
|
| 122 |
-
assert keyed["tasks"] == [{"id": "easy", "description": "one"}, {"id": "hard", "description": "two"}]
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def test_validation_block():
|
| 126 |
-
m = sf.parse_manifest(CANONICAL + """validation:
|
| 127 |
-
reward: {range: [0.0, 1.0], oracle_tolerance: 0.01, floor_margin: 0.5, variance_tolerance: 0.05}
|
| 128 |
-
judge: {model: Qwen/Qwen3-8B, version: "2025-05", params: {temperature: 0}}
|
| 129 |
-
resources: {cpu: 2, memory_mb: 4096, disk_mb: 2048, episode_timeout_s: 600}
|
| 130 |
-
network: {mode: allowlist, allowed_hosts: [api.example.com]}
|
| 131 |
-
capabilities:
|
| 132 |
-
verifier: {kind: reward_channel}
|
| 133 |
-
llm_judged: true
|
| 134 |
-
task_api: true
|
| 135 |
-
declared_tools: [look, submit_guess]
|
| 136 |
-
declared_task_count: {train: 1000, eval: 200}
|
| 137 |
-
types: {tags: [geo]}
|
| 138 |
-
""", "openenv.yaml")
|
| 139 |
-
v = m["validation"]
|
| 140 |
-
assert v["reward"]["range"] == [0.0, 1.0] and v["reward"]["variance_tolerance"] == 0.05
|
| 141 |
-
assert v["judge"]["model"] == "Qwen/Qwen3-8B"
|
| 142 |
-
assert v["capabilities"]["llm_judged"] is True and v["capabilities"]["verifier"]["kind"] == "reward_channel"
|
| 143 |
-
assert v["capabilities"]["declared_tools"] == ["look", "submit_guess"]
|
| 144 |
-
assert v["capabilities"]["declared_task_count"] == {"train": 1000, "eval": 200}
|
| 145 |
-
assert v["network"] == {"mode": "allowlist", "allowed_hosts": ["api.example.com"]}
|
| 146 |
-
assert v["resources"]["memory_mb"] == 4096 and v["types"] == ["geo"]
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
def test_broken_and_explosive_yaml_is_said_so_quickly():
|
| 150 |
-
assert sf.parse_manifest("name: [unclosed\n", "openenv.yaml")["error"].startswith("not valid YAML")
|
| 151 |
-
assert sf.parse_manifest("- just\n- a list\n", "openenv.yaml")["error"] == "not a YAML mapping"
|
| 152 |
-
laughs = "a: &a [x, x, x, x, x, x, x, x, x]\n" + "".join(f"{c}: &{c} [*{p}, *{p}, *{p}, *{p}, *{p}, *{p}, *{p}, *{p}, *{p}]\n"
|
| 153 |
-
for p, c in zip("abcdefgh", "bcdefghi")) + "rubric: *i\n"
|
| 154 |
-
t = time.time()
|
| 155 |
-
m = sf.parse_manifest(laughs, "openenv.yaml")
|
| 156 |
-
assert time.time() - t < 2 and len(json.dumps(m)) < 2_000_000
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
def test_the_shallowest_manifest_wins_and_ties_follow_the_dockerfile():
|
| 160 |
-
paths = ["envs/deep/x/openenv.yaml", "env/openenv.yaml", "geoguesser_env/openenv.yaml", "src/openenv/cli/templates/openenv_env/openenv.yaml"]
|
| 161 |
-
assert sf.manifest_path(paths, "FROM python:3.12\nCOPY geoguesser_env /app/geoguesser_env\n", "FineEnvs/geoguesser-env") == "geoguesser_env/openenv.yaml"
|
| 162 |
-
assert sf.manifest_path(paths, None, "FineEnvs/geoguesser-env") == "geoguesser_env/openenv.yaml" # named like the Space
|
| 163 |
-
assert sf.manifest_path(paths, None, "someone/other") == "env/openenv.yaml"
|
| 164 |
-
assert sf.manifest_path(["a/openenv.yaml", "openenv.yaml"], None, "o/n") == "openenv.yaml"
|
| 165 |
-
assert sf.manifest_path(["src/openenv/cli/templates/openenv_env/openenv.yaml"], None, "o/n") is None
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
def test_a_subfolder_manifest_roots_the_environment(monkeypatch, tmp_path):
|
| 169 |
-
files = {"Dockerfile": "FROM python:3.12\nCOPY my_env /app/my_env\nENV ENABLE_WEB_INTERFACE=true\n",
|
| 170 |
-
"my_env/openenv.yaml": CANONICAL.replace("echo_env", "my_env"),
|
| 171 |
-
"my_env/models.py": "from openenv.core.env_server.types import Action, Observation\nclass A(Action):\n x: int = 1\n",
|
| 172 |
-
"my_env/server/app.py": "app = create_app(E, A, O, env_name='my_env')\n",
|
| 173 |
-
"my_env/server/my_environment.py": "class E(Environment):\n pass\n",
|
| 174 |
-
"my_env/pyproject.toml": '[project]\nname = "openenv-my-env"\ndependencies = ["openenv-core>=0.2.1"]\n'}
|
| 175 |
-
stub_space(monkeypatch, tmp_path, files)
|
| 176 |
-
d = sf.declared("org/env")
|
| 177 |
-
assert d["root"] == "my_env" and d["sha"] == SHA and d["short"] == SHA[:7]
|
| 178 |
-
assert d["keys"]["manifest"] == "my_env/openenv.yaml" and d["keys"]["models"] == "my_env/models.py"
|
| 179 |
-
assert d["declared"]["manifest"]["keys"]["name"] == "my_env"
|
| 180 |
-
assert d["declared"]["app"]["env_name"] == "my_env"
|
| 181 |
-
assert d["declared"]["dockerfile"]["web_interface"] is True
|
| 182 |
-
assert d["declared"]["pyproject"]["openenv_version"] == ">=0.2.1"
|
| 183 |
-
assert d["declared"]["card"]["base_path"] == "/web"
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
# ── Python, parsed and never run ─────────────────────────────────────────────
|
| 187 |
-
MODELS = '''
|
| 188 |
-
import os
|
| 189 |
-
os.system("touch {marker}")
|
| 190 |
-
raise SystemExit("models.py must never run here")
|
| 191 |
-
|
| 192 |
-
from enum import Enum
|
| 193 |
-
from pydantic import Field
|
| 194 |
-
from openenv.core.env_server.types import Action, Observation, State
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
class Mode(str, Enum):
|
| 198 |
-
EASY = "easy"
|
| 199 |
-
HARD = "hard"
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
class Base(Action):
|
| 203 |
-
"""Every action."""
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
class MoveAction(Base):
|
| 207 |
-
"""Walk along the road.
|
| 208 |
-
|
| 209 |
-
More detail."""
|
| 210 |
-
direction: str = Field(..., description="forward or backward", pattern="^(forward|backward)$")
|
| 211 |
-
meters: float = Field(default=10.0, gt=0, description="how far")
|
| 212 |
-
tags: list[str] = Field(default_factory=list)
|
| 213 |
-
_private: int = 0
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
class GeoObservation(Observation):
|
| 217 |
-
image_b64: str = Field(default="", description="the view, as base64 PNG")
|
| 218 |
-
feedback: str = ""
|
| 219 |
-
model_config = {{"extra": "forbid"}}
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
class GeoState(State):
|
| 223 |
-
step_count: int = 0
|
| 224 |
-
'''
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
def test_models_are_parsed_and_never_executed(tmp_path):
|
| 228 |
-
marker = tmp_path / "pwned"
|
| 229 |
-
m = sf.parse_models(MODELS.format(marker=marker), "models.py") # would raise SystemExit, or touch the marker, if run
|
| 230 |
-
assert not marker.exists()
|
| 231 |
-
by = {c["name"]: c for c in m["classes"]}
|
| 232 |
-
assert by["MoveAction"]["role"] == "action" and by["Base"]["role"] == "action" # through Base, within the file
|
| 233 |
-
assert by["GeoObservation"]["role"] == "observation" and by["GeoState"]["role"] == "state"
|
| 234 |
-
assert by["Mode"]["kind"] == "enum" and by["Mode"]["values"] == ["'easy'", "'hard'"] and by["Mode"]["role"] is None
|
| 235 |
-
assert by["MoveAction"]["doc"] == "Walk along the road."
|
| 236 |
-
f = {x["name"]: x for x in by["MoveAction"]["fields"]}
|
| 237 |
-
assert set(f) == {"direction", "meters", "tags"} # private fields left out
|
| 238 |
-
assert f["direction"]["required"] is True and f["direction"]["description"] == "forward or backward"
|
| 239 |
-
assert f["meters"]["default"] == "10.0" and not f["meters"]["required"] and "gt 0" in f["meters"]["limits"]
|
| 240 |
-
assert f["tags"]["default"] == "list()" and f["tags"]["type"] == "list[str]"
|
| 241 |
-
o = {x["name"]: x for x in by["GeoObservation"]["fields"]}
|
| 242 |
-
assert o["image_b64"].get("image") is True and "model_config" not in o
|
| 243 |
-
assert sf.parse_models("def broken(:\n", "models.py")["error"]
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
def test_server_app_and_environment_are_read_not_run(tmp_path):
|
| 247 |
-
marker = tmp_path / "ran"
|
| 248 |
-
app_py = f'''
|
| 249 |
-
import os
|
| 250 |
-
open("{marker}", "w").write("x")
|
| 251 |
-
MAX_CONCURRENT = int(os.getenv("MAX_CONCURRENT_ENVS", "4"))
|
| 252 |
-
app = create_app(GeoEnvironment, GeoAction, GeoObservation, env_name="geo", max_concurrent_envs=MAX_CONCURRENT,
|
| 253 |
-
gradio_builder=build_ui, custom_tab_name="Play")
|
| 254 |
-
|
| 255 |
-
@app.get("/geo/task/{{i}}")
|
| 256 |
-
def task(i: int): ...
|
| 257 |
-
'''
|
| 258 |
-
a = sf.parse_app(app_py, "server/app.py")
|
| 259 |
-
assert not marker.exists()
|
| 260 |
-
assert (a["env"], a["action"], a["observation"], a["env_name"]) == ("GeoEnvironment", "GeoAction", "GeoObservation", "geo")
|
| 261 |
-
assert a["max_concurrent_envs"] == 4 and a["gradio_builder"] == "build_ui" and a["custom_tab_name"] == "Play"
|
| 262 |
-
assert a["routes"] == ["GET /geo/task/{i}"] and not a["mcp"]
|
| 263 |
-
assert sf.parse_app("app = create_app(E, CallToolAction, CallToolObservation, env_name='echo')", "server/app.py")["mcp"] is True
|
| 264 |
-
|
| 265 |
-
env = sf.parse_environment('''
|
| 266 |
-
from openai import OpenAI
|
| 267 |
-
from .scoring import compute_reward
|
| 268 |
-
from openenv.core.env_server.mcp_environment import MCPEnvironment
|
| 269 |
-
|
| 270 |
-
class GeoEnvironment(MCPEnvironment):
|
| 271 |
-
def _register(self, mcp):
|
| 272 |
-
@mcp.tool
|
| 273 |
-
def look(heading_deg: float, fov_deg: float = 90):
|
| 274 |
-
"""Render a view."""
|
| 275 |
-
@mcp.tool()
|
| 276 |
-
def submit_guess(lat: float, lon: float):
|
| 277 |
-
"""Commit the answer."""
|
| 278 |
-
|
| 279 |
-
def _grade(self, guess):
|
| 280 |
-
return compute_reward(guess)
|
| 281 |
-
''', "server/geo_environment.py")
|
| 282 |
-
assert [t["name"] for t in env["tools"]] == ["look", "submit_guess"]
|
| 283 |
-
assert env["tools"][0]["params"] == ["heading_deg", "fov_deg"] and env["tools"][0]["description"] == "Render a view."
|
| 284 |
-
assert env["llm_judge"] is True and env["mcp"] is True and "_grade" in env["functions"]
|
| 285 |
-
assert sf.reward_files("server/geo_environment.py", env["reward_imports"], {"server/scoring.py"}, "") == ["server/scoring.py"]
|
| 286 |
-
assert sf.parse_environment("from openenv.core.rubrics import Rubric\nclass R(Rubric): pass\n", "e.py")["rubric"] is True
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
def test_dockerfile_and_pyproject_facts():
|
| 290 |
-
d = sf.parse_dockerfile("ARG BASE_IMAGE=ghcr.io/meta-pytorch/openenv-base:latest\nFROM ${BASE_IMAGE} AS builder\n"
|
| 291 |
-
"FROM ${BASE_IMAGE}\nENV ENABLE_WEB_INTERFACE=true\nEXPOSE 7860\n"
|
| 292 |
-
'CMD ["uvicorn", "server.app:app", "--host", "0.0.0.0", "--port", "8000"]\n', "Dockerfile")
|
| 293 |
-
assert d["from"] == ["ghcr.io/meta-pytorch/openenv-base:latest"] and d["web_interface"] is True
|
| 294 |
-
assert d["expose"] == 7860 and d["port"] == 8000
|
| 295 |
-
assert sf.parse_dockerfile("FROM python:3.12\nENV ENABLE_WEB_INTERFACE false\n", "Dockerfile")["web_interface"] is False
|
| 296 |
-
git = sf.parse_pyproject('[project]\nname = "openenv-echo-env"\ndependencies = ["fastapi", '
|
| 297 |
-
'"openenv-core[core] @ git+https://github.com/meta-pytorch/OpenEnv.git@v0.2.3"]\n'
|
| 298 |
-
'[project.scripts]\nserver = "echo_env.server.app:main"\n', "pyproject.toml")
|
| 299 |
-
assert git["package"] == "openenv-echo-env" and git["openenv_version"] == "0.2.3" and git["server_script"] == "echo_env.server.app:main"
|
| 300 |
-
assert sf.parse_pyproject('[project]\ndependencies = ["openenv==0.3.1"]\n', "p")["openenv_version"] == "0.3.1"
|
| 301 |
-
assert "openenv" not in sf.parse_pyproject('[project]\ndependencies = ["openenv-echo-env>=1"]\n', "p") # another package
|
| 302 |
-
assert sf.parse_pyproject("not = [toml", "p")["error"]
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
# ── the tree ────────────────────────────────────────────────────────────────��
|
| 306 |
-
def test_junk_is_left_out_of_the_tree():
|
| 307 |
-
info = {"files": [(p, 10, False) for p in (
|
| 308 |
-
".gitattributes", "README.md", "openenv.yaml", "models.py", "server/app.py", "uv.lock",
|
| 309 |
-
"src/openenv/core/env_server/http_server.py", "src/core/env_client.py", "src/openenv_core/__init__.py",
|
| 310 |
-
"build/lib/server/app.py", "envs/echo_env/build/lib/echo_env/client.py", "openenv_echo_env.egg-info/PKG-INFO",
|
| 311 |
-
"server/__pycache__/app.cpython-312.pyc", "envs/echo_env/server/app.py", "solution/solve.py",
|
| 312 |
-
"tasks/eval.jsonl", "data/geo/countries.geojson")] + [("assets/map.png", 10, True)]}
|
| 313 |
-
files, dropped, truncated = sf.listing(info)
|
| 314 |
-
paths = [f["path"] for f in files]
|
| 315 |
-
assert paths == sorted(["README.md", "openenv.yaml", "models.py", "server/app.py", "uv.lock", "envs/echo_env/server/app.py",
|
| 316 |
-
"solution/solve.py", "tasks/eval.jsonl", "data/geo/countries.geojson", "assets/map.png"])
|
| 317 |
-
assert dropped == 8 and not truncated
|
| 318 |
-
by = {f["path"]: f for f in files}
|
| 319 |
-
assert by["uv.lock"]["collapsed"] and by["assets/map.png"]["binary"]
|
| 320 |
-
assert by["solution/solve.py"]["withheld"] and by["tasks/eval.jsonl"]["withheld"]
|
| 321 |
-
assert not by["data/geo/countries.geojson"].get("withheld") and not by["openenv.yaml"].get("withheld")
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
# ── one file ─────────────────────────────────────────────────────────────────
|
| 325 |
-
def test_the_file_endpoint_serves_only_listed_text(monkeypatch, tmp_path):
|
| 326 |
-
big = "x" * (sf.MAX_TEXT + 10)
|
| 327 |
-
calls = stub_space(monkeypatch, tmp_path, {
|
| 328 |
-
"openenv.yaml": CANONICAL, "models.py": "class A: pass\n", "big.txt": big,
|
| 329 |
-
"blob.dat": b"\x00\x01binary", "picture.png": b"\x89PNG", "config/answers.json": '{"x": 1}',
|
| 330 |
-
"data/manifest.json": '{"steps": [{"original_bash": "ls"}]}',
|
| 331 |
-
"uv.lock": 'version = 1\n[[package]]\nname = "fastapi"\nversion = "0.115.0"\n[[package]]\nname = "openenv-core"\nversion = "0.2.3"\n',
|
| 332 |
-
}, extra=["src/openenv/core/secret.py"])
|
| 333 |
-
r = client.get("/api/spaces/org/env/file", params={"f": "models.py"})
|
| 334 |
-
assert r.status_code == 200 and r.json()["text"] == "class A: pass\n" and r.json()["sha"] == SHA
|
| 335 |
-
for bad in ("../etc/passwd", "/etc/passwd", "server/../models.py", "a\\b", "./models.py", ""):
|
| 336 |
-
assert client.get("/api/spaces/org/env/file", params={"f": bad}).status_code in (400, 404), bad
|
| 337 |
-
assert client.get("/api/spaces/org/env/file", params={"f": "src/openenv/core/secret.py"}).status_code == 404 # junk: not served
|
| 338 |
-
assert client.get("/api/spaces/org/env/file", params={"f": "nothere.py"}).status_code == 404
|
| 339 |
-
assert client.get("/api/spaces/org/env/file", params={"f": "picture.png"}).status_code == 415 # binary by name
|
| 340 |
-
assert client.get("/api/spaces/org/env/file", params={"f": "blob.dat"}).status_code == 415 # binary by content
|
| 341 |
-
long = client.get("/api/spaces/org/env/file", params={"f": "big.txt"}).json()
|
| 342 |
-
assert long["truncated"] and len(long["text"]) == sf.MAX_TEXT
|
| 343 |
-
for answers in ("config/answers.json", "data/manifest.json"): # by name; by its keys
|
| 344 |
-
w = client.get("/api/spaces/org/env/file", params={"f": answers}).json()
|
| 345 |
-
assert w["withheld"] and "text" not in w, answers
|
| 346 |
-
lock = client.get("/api/spaces/org/env/file", params={"f": "uv.lock"}).json()
|
| 347 |
-
assert lock["collapsed"] and "openenv-core 0.2.3" in lock["text"] and "[[package]]" not in lock["text"]
|
| 348 |
-
assert {c["file"] for c in calls} <= {"models.py", "big.txt", "blob.dat", "data/manifest.json", "uv.lock"} # never the junk or the png
|
| 349 |
-
assert all(c["token"] is False and c["revision"] == SHA and c["type"] == "space" for c in calls)
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
def test_files_endpoint_shape_and_no_token(monkeypatch, tmp_path):
|
| 353 |
-
tokens = []
|
| 354 |
-
stub_space(monkeypatch, tmp_path, {"openenv.yaml": CANONICAL, "README.md": "# hi\n"}, extra=["src/core/x.py", "build/lib/a.py"], seen_tokens=tokens)
|
| 355 |
-
r = client.get("/api/spaces/org/env/files", headers={"Authorization": "Bearer hf_visitor_token"})
|
| 356 |
-
assert r.status_code == 200
|
| 357 |
-
d = r.json()
|
| 358 |
-
assert [f["path"] for f in d["files"]] == ["README.md", "openenv.yaml"] and d["dropped"] == 2
|
| 359 |
-
assert d["subdomain"] == "org-env" and d["hardware"] == "cpu-basic" and d["declared"]["manifest"]["keys"]["name"] == "echo_env"
|
| 360 |
-
assert tokens and all(t is None for _, t in tokens) # catalog._api(): anonymous
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
def test_private_spaces_are_refused(monkeypatch, tmp_path):
|
| 364 |
-
stub_space(monkeypatch, tmp_path, {"openenv.yaml": CANONICAL}, private=True)
|
| 365 |
-
assert client.get("/api/spaces/org/env/files").status_code == 404
|
| 366 |
-
assert client.get("/api/spaces/org/env/file", params={"f": "openenv.yaml"}).status_code == 404
|
| 367 |
-
from huggingface_hub.errors import RepositoryNotFoundError
|
| 368 |
-
|
| 369 |
-
def missing(spec, **kw):
|
| 370 |
-
raise RepositoryNotFoundError("nope", response=SimpleNamespace(status_code=404, headers={}, request=None))
|
| 371 |
-
|
| 372 |
-
catalog._memo.clear()
|
| 373 |
-
monkeypatch.setattr(catalog, "_api", lambda token=None: SimpleNamespace(space_info=missing))
|
| 374 |
-
assert client.get("/api/spaces/org/env/files").status_code == 404
|
| 375 |
-
assert client.get("/api/spaces/../x/files").status_code in (400, 404)
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
# ── last seen, and openenv validate's criteria ───────────────────────────────
|
| 379 |
-
SCHEMA = {"action": {"properties": {"message": {"type": "string"}}}, "observation": {"properties": {"reward": {}}}, "state": {"properties": {}}}
|
| 380 |
-
OPENAPI = {"info": {"version": "1.0.0"}, "paths": {p: {} for p in ("/reset", "/step", "/state", "/metadata", "/health", "/schema", "/mcp")}}
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
def stub_running(monkeypatch, stage="RUNNING"):
|
| 384 |
-
rec = {"id": "org/env", "host": "https://org-env.hf.space", "stage": stage, "base_path": "/web"}
|
| 385 |
-
monkeypatch.setattr(live, "record", lambda spec, fresh=False: rec)
|
| 386 |
-
answers = {"/openapi.json": OPENAPI, "/metadata": {"name": "env", "description": "an env", "readme_content": "x" * 5000},
|
| 387 |
-
"/schema": SCHEMA, "/health": {"status": "healthy"}, "/list_environments": None}
|
| 388 |
-
monkeypatch.setattr(live, "_get_json", lambda rec, path: answers.get(path))
|
| 389 |
-
monkeypatch.setattr(live, "_ui_path", lambda rec: "/web/")
|
| 390 |
-
monkeypatch.setattr(live, "_tools", lambda rec: ([{"name": "echo", "inputSchema": {"type": "object"}}], None, {"ok": True, "detail": "JSON-RPC 2.0"}))
|
| 391 |
-
return rec
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
def test_a_good_probe_is_kept_and_shown_while_the_space_sleeps(monkeypatch):
|
| 395 |
-
rec = stub_running(monkeypatch)
|
| 396 |
-
info = live.probe("org/env")
|
| 397 |
-
assert info["running"] and info["mode"] == "simulation" and info["openapi_version"] == "1.0.0"
|
| 398 |
-
assert [c["status"] for c in info["conformance"]] == ["pass"] * 6
|
| 399 |
-
p = Path(config.STORAGE_DIR) / "space-probes" / "org__env.json"
|
| 400 |
-
assert p.exists() and not [x for x in p.parent.iterdir() if x.name.endswith(".tmp")] # written whole
|
| 401 |
-
kept = json.loads(p.read_text())
|
| 402 |
-
assert kept["mcp"][0]["name"] == "echo" and kept["schema"] == SCHEMA and kept["ui"] == "/web/"
|
| 403 |
-
assert kept["metadata"] == {"name": "env", "description": "an env"} # no README copy
|
| 404 |
-
rec["stage"] = "SLEEPING"
|
| 405 |
-
catalog._memo.clear()
|
| 406 |
-
asleep = live.probe("org/env")
|
| 407 |
-
assert not asleep["running"] and asleep["last_seen"]["mcp"][0]["name"] == "echo"
|
| 408 |
-
assert asleep["last_seen"]["checked_at"] <= time.time() and len(asleep["last_seen"]["conformance"]) == 6
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
def test_a_probe_that_got_nothing_does_not_replace_the_last_good_one(monkeypatch):
|
| 412 |
-
stub_running(monkeypatch)
|
| 413 |
-
live.probe("org/env")
|
| 414 |
-
catalog._memo.clear()
|
| 415 |
-
monkeypatch.setattr(live, "_get_json", lambda rec, path: None)
|
| 416 |
-
monkeypatch.setattr(live, "_tools", lambda rec: (None, "the Space's /mcp answered HTTP 503", {"ok": False, "detail": "HTTP 503"}))
|
| 417 |
-
bad = live.probe("org/env")
|
| 418 |
-
assert bad["running"] and bad["conformance"][0]["status"] == "fail"
|
| 419 |
-
assert live.last_seen("org/env")["mcp"][0]["name"] == "echo"
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
def test_the_last_seen_record_is_trimmed_to_fit():
|
| 423 |
-
huge = [{"name": f"t{i}", "inputSchema": {"description": "x" * 4000}, "outputSchema": {"d": "y" * 4000}} for i in range(120)]
|
| 424 |
-
kept = live._seen_record("org/env", {"mcp": huge, "schema": SCHEMA, "checked": 1.0})
|
| 425 |
-
assert len(json.dumps(kept)) <= live.SEEN_MAX and [t["name"] for t in kept["mcp"]] == [f"t{i}" for i in range(120)]
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
def test_conformance_criteria():
|
| 429 |
-
allgood = {"openapi": OPENAPI, "health": {"status": "healthy"}, "metadata": {"name": "a", "description": "b"}, "schema": SCHEMA,
|
| 430 |
-
"mcp": {"ok": True, "detail": "JSON-RPC 2.0"}, "mcp_tried": True, "paths": list(OPENAPI["paths"])}
|
| 431 |
-
ids = [c["id"] for c in live.conformance(allgood)]
|
| 432 |
-
assert ids == ["openapi_version_available", "health_endpoint", "metadata_endpoint", "schema_endpoint", "mcp_endpoint", "mode_endpoint_consistency"]
|
| 433 |
-
assert all(c["status"] == "pass" for c in live.conformance(allgood))
|
| 434 |
-
|
| 435 |
-
def status(facts):
|
| 436 |
-
return {c["id"]: c["status"] for c in live.conformance({**allgood, **facts})}
|
| 437 |
-
|
| 438 |
-
assert status({"openapi": {"paths": {}}})["openapi_version_available"] == "fail"
|
| 439 |
-
assert status({"health": {"status": "ok"}})["health_endpoint"] == "fail"
|
| 440 |
-
assert status({"metadata": {"name": "a"}})["metadata_endpoint"] == "fail"
|
| 441 |
-
assert status({"schema": {"action": {}, "observation": {}}})["schema_endpoint"] == "fail"
|
| 442 |
-
assert status({"mcp": {"ok": False, "detail": "HTTP 404"}})["mcp_endpoint"] == "fail"
|
| 443 |
-
assert status({"mcp": None})["mcp_endpoint"] == "fail" # not among its routes
|
| 444 |
-
assert status({"mcp": None, "paths": None})["mcp_endpoint"] == "unknown"
|
| 445 |
-
assert status({"paths": ["/mcp", "/health"]})["mode_endpoint_consistency"] == "pass" # production
|
| 446 |
-
assert status({"paths": ["/mcp", "/step"]})["mode_endpoint_consistency"] == "fail" # production with /step
|
| 447 |
-
assert status({"paths": ["/reset", "/step"]})["mode_endpoint_consistency"] == "fail" # simulation without /state
|
| 448 |
-
assert status({"paths": None})["mode_endpoint_consistency"] == "fail"
|
| 449 |
-
assert {c["status"] for c in live.conformance(None)} == {"unknown"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_space_tasks.py
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
import time
|
| 2 |
-
|
| 3 |
-
import pytest
|
| 4 |
-
|
| 5 |
-
from app import space_tasks as tasks, spaces_live as live
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
def test_multiple_environments_count_once_and_keep_unknowns(monkeypatch):
|
| 9 |
-
docs = {"/list_environments": ["a", "a", "b", "../bad"],
|
| 10 |
-
"/a/splits": [{"name": "train", "num_tasks": 12}, {"name": "train", "num_tasks": 12}],
|
| 11 |
-
"/b/splits": [{"name": "test", "num_tasks": True}, {"name": "valid", "num_tasks": 0}]}
|
| 12 |
-
monkeypatch.setattr(live, "_get_json", lambda rec, path: docs[path])
|
| 13 |
-
d = tasks.discover({}, {"/{env_name}/splits"})
|
| 14 |
-
assert [e["env"] for e in d["environments"]] == ["a", "b"]
|
| 15 |
-
s = tasks.summary(d)
|
| 16 |
-
assert s["tasks"] == 12 and s["counted_splits"] == 2 and s["unknown_splits"] == 1
|
| 17 |
-
assert not s["complete"]
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
@pytest.mark.parametrize("value", [True, -1, "100", 2.5, float("inf"), 10**30])
|
| 21 |
-
def test_invalid_counts_never_inflate_totals(value):
|
| 22 |
-
assert tasks.count(value) is None
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
def test_census_filters_visibility_expiry_and_inactive_spaces():
|
| 26 |
-
now = time.time()
|
| 27 |
-
s = {"tasks": 19, "counted_splits": 1, "checked_at": now, "complete": True}
|
| 28 |
-
records = {"a": {"stage": "RUNNING", "task_catalog": s},
|
| 29 |
-
"private": {"stage": "RUNNING", "task_catalog": s},
|
| 30 |
-
"asleep": {"stage": "SLEEPING", "task_catalog": s},
|
| 31 |
-
"stale": {"stage": "RUNNING", "task_catalog": {**s, "checked_at": now - tasks.FRESH}}}
|
| 32 |
-
assert tasks.census(records, ["a", "asleep", "stale"], now)["tasks"] == 19
|
| 33 |
-
assert tasks.census(records, ["a"], now - 1)["tasks"] == 0
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
def test_task_environment_must_be_advertised(monkeypatch):
|
| 37 |
-
monkeypatch.setattr(live, "probe", lambda spec: {"running": True, "task_api": {"env": "a", "environments": [{"env": "a"}, {"env": "b"}]}})
|
| 38 |
-
monkeypatch.setattr(live, "record", lambda spec: {})
|
| 39 |
-
assert live._task_api("o/s", "b")[1] == "b"
|
| 40 |
-
with pytest.raises(live.SpaceError):
|
| 41 |
-
live._task_api("o/s", "../secrets")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/ui-audit.mjs
DELETED
|
@@ -1,320 +0,0 @@
|
|
| 1 |
-
// UI audit of the HF RL Explorer: every check clicks or types like a person would and asserts the result, then
|
| 2 |
-
// every page is checked at four widths in both themes for errors, sideways scroll, broken images and unlabelled
|
| 3 |
-
// buttons, with a screenshot of each.
|
| 4 |
-
// npm i -D playwright && npx playwright install chromium (or PW_CHROME=/path/to/chrome-headless-shell)
|
| 5 |
-
// node tests/ui-audit.mjs http://localhost:8060 /tmp/rlx-shots
|
| 6 |
-
// Uses live Hub data and live Spaces (FineEnvs/geoguesser-env, FineEnvs/latex-ocr-env, ...): it needs network.
|
| 7 |
-
import { chromium } from "playwright";
|
| 8 |
-
import { mkdirSync } from "node:fs";
|
| 9 |
-
|
| 10 |
-
const BASE = process.argv[2] || "http://localhost:8060";
|
| 11 |
-
const SHOTS = process.argv[3] || "/tmp/rlx-shots";
|
| 12 |
-
mkdirSync(SHOTS, { recursive: true });
|
| 13 |
-
const HARBOR = "harborframework/terminal-bench-2.1", HTASK = "tasks/feal-differential-cryptanalysis";
|
| 14 |
-
const MIMO = "XiaomiMiMo/MiMo-V2.6-RL-oss", MIMO_GENERAL = "s3k_0052_accounting_audit_tax_en_t2_rl_008";
|
| 15 |
-
const results = [];
|
| 16 |
-
const ok = (name, cond, detail = "") => { results.push({ name, pass: !!cond, detail }); if (!cond) console.log(" FAIL", name, detail); };
|
| 17 |
-
|
| 18 |
-
const browser = await chromium.launch(process.env.PW_CHROME ? { executablePath: process.env.PW_CHROME } : {});
|
| 19 |
-
|
| 20 |
-
async function page(width, theme = "light", height = 900) {
|
| 21 |
-
const ctx = await browser.newContext({ viewport: { width, height }, deviceScaleFactor: 1, hasTouch: width < 768, isMobile: width < 768 });
|
| 22 |
-
await ctx.addInitScript((t) => { try { localStorage.setItem("theme", t); localStorage.removeItem("fe-banner-closed"); } catch {} }, theme);
|
| 23 |
-
const p = await ctx.newPage();
|
| 24 |
-
p.errors = [];
|
| 25 |
-
p.on("pageerror", (e) => p.errors.push("pageerror: " + e.message));
|
| 26 |
-
p.on("console", (m) => { if (m.type() === "error" && !/favicon|Failed to load resource|net::ERR|cdn-thumbnails|hf\.space/.test(m.text())) p.errors.push("console: " + m.text()); });
|
| 27 |
-
p.on("dialog", (d) => { p.errors.push("native dialog: " + d.message()); d.dismiss(); });
|
| 28 |
-
p.on("response", (r) => { const u = r.url(); if (u.startsWith(BASE) && (r.status() === 403 || r.status() >= 500)) p.errors.push(`${r.status()} ${r.request().method()} ${u.slice(BASE.length, BASE.length + 120)}`); });
|
| 29 |
-
return p;
|
| 30 |
-
}
|
| 31 |
-
// a live Space's interface fills in once its server answers the probe: wait for that, not for the section's skeleton
|
| 32 |
-
const liveIn = async (p, ms = 60000) => { try { await p.waitForFunction(() => /live/.test(document.querySelector("#sec-interface")?.textContent || "") && document.querySelector(".pg-opts, #pg-route, #pg-reset"), null, { timeout: ms }); return true; } catch { return false; } };
|
| 33 |
-
const here = (p) => p.evaluate(() => location.pathname + location.search + location.hash); // pages are paths now; old #/ links redirect
|
| 34 |
-
const go = async (p, to, wait = 1200) => { await p.goto(BASE + "/" + to.replace(/^\/+/, ""), { waitUntil: "domcontentloaded" }); await p.waitForTimeout(wait); }; // "#/old", "path" or "/path"
|
| 35 |
-
const until = async (p, sel, ms = 20000) => { try { await p.waitForSelector(sel, { timeout: ms }); return true; } catch { return false; } };
|
| 36 |
-
|
| 37 |
-
async function health(p, label) {
|
| 38 |
-
const r = await p.evaluate(() => {
|
| 39 |
-
const over = document.documentElement.scrollWidth > document.documentElement.clientWidth + 1;
|
| 40 |
-
const wide = over ? [...document.querySelectorAll("body *")].filter((e) => e.getBoundingClientRect().right > document.documentElement.clientWidth + 2
|
| 41 |
-
&& getComputedStyle(e).position !== "fixed" && e.offsetParent).slice(0, 4).map((e) => `${e.tagName.toLowerCase()}.${[...e.classList].join(".")}`) : [];
|
| 42 |
-
const broken = [...document.images].filter((i) => i.complete && i.naturalWidth === 0 && i.src && !i.src.includes("cdn-thumbnails") && i.offsetParent).map((i) => i.src.slice(0, 80));
|
| 43 |
-
const unnamed = [...document.querySelectorAll("button, a[href]")].filter((b) => b.offsetParent && !(b.textContent.trim() || b.getAttribute("aria-label") || b.getAttribute("title"))).map((b) => b.outerHTML.slice(0, 90));
|
| 44 |
-
const cut = [];
|
| 45 |
-
for (const e of document.querySelectorAll("body *")) {
|
| 46 |
-
if (!e.offsetParent || ![...e.childNodes].some((n) => n.nodeType === 3 && n.textContent.trim())) continue;
|
| 47 |
-
if (getComputedStyle(e).textOverflow === "ellipsis") continue;
|
| 48 |
-
const r = e.getBoundingClientRect();
|
| 49 |
-
for (let a = e.parentElement; a && a !== document.body; a = a.parentElement) {
|
| 50 |
-
const ox = getComputedStyle(a).overflowX;
|
| 51 |
-
if (ox === "auto" || ox === "scroll") break;
|
| 52 |
-
if (ox !== "visible") {
|
| 53 |
-
if (getComputedStyle(a).textOverflow === "ellipsis") break; // cut on purpose, with an ellipsis
|
| 54 |
-
const ar = a.getBoundingClientRect();
|
| 55 |
-
if (r.right > ar.right + 1 && !a.matches(".sp-facts")) cut.push(`${e.tagName.toLowerCase()}.${[...e.classList].join(".")} "${e.textContent.trim().slice(0, 30)}"`);
|
| 56 |
-
break;
|
| 57 |
-
}
|
| 58 |
-
}
|
| 59 |
-
}
|
| 60 |
-
// content spilling out of its panel, card or column over its neighbour (nothing clips it, so "cut" misses it)
|
| 61 |
-
const spill = [];
|
| 62 |
-
const inScroller = (e, box) => { for (let a = e.parentElement; a && a !== box; a = a.parentElement) { const s = getComputedStyle(a); if (s.overflowX !== "visible" || s.textOverflow === "ellipsis") return true; } return false; };
|
| 63 |
-
for (const box of document.querySelectorAll(".panel, .card, .pg-ctl, #pg-log, .tp-side, .sp-side")) {
|
| 64 |
-
if (!box.offsetParent) continue;
|
| 65 |
-
const br = box.getBoundingClientRect();
|
| 66 |
-
for (const e of box.querySelectorAll("*")) {
|
| 67 |
-
if (!e.offsetParent || getComputedStyle(e).position === "absolute") continue;
|
| 68 |
-
if (e.getBoundingClientRect().right > br.right + 2 && !inScroller(e, box)) { spill.push(`${e.tagName.toLowerCase()}.${[...e.classList].join(".")} out of ${box.tagName.toLowerCase()}.${[...box.classList].join(".") || box.id}`); break; }
|
| 69 |
-
}
|
| 70 |
-
}
|
| 71 |
-
return { over, wide, broken, unnamed, cut: [...new Set(cut)], spill: [...new Set(spill)] };
|
| 72 |
-
});
|
| 73 |
-
ok(`${label}: no sideways scroll`, !r.over, r.wide.join(" "));
|
| 74 |
-
ok(`${label}: no broken images`, !r.broken.length, r.broken.join(" "));
|
| 75 |
-
ok(`${label}: every button and link is named`, !r.unnamed.length, r.unnamed.slice(0, 3).join(" | "));
|
| 76 |
-
ok(`${label}: no text cut off by its card or panel`, !r.cut.length, r.cut.slice(0, 3).join(" | "));
|
| 77 |
-
ok(`${label}: nothing spills out of its panel or column`, !r.spill.length, r.spill.slice(0, 3).join(" | "));
|
| 78 |
-
ok(`${label}: no errors`, !p.errors.length, p.errors.slice(0, 4).join(" | "));
|
| 79 |
-
p.errors = [];
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
// ── desktop: interactions ────────────────────────────────────────────────────
|
| 83 |
-
{
|
| 84 |
-
const p = await page(1440, "dark");
|
| 85 |
-
await go(p, "#/", 600);
|
| 86 |
-
ok("home: the app replaces the boot loader", await until(p, "#list .card"));
|
| 87 |
-
ok("home: no collection tiles", !(await p.$("#domains, .domains")));
|
| 88 |
-
ok("home: one search box (the header's hides on Explore)", await p.evaluate(() => getComputedStyle(document.querySelector(".top-search")).visibility === "hidden"));
|
| 89 |
-
ok("home: the FineEnvs banner is one link", await p.evaluate(() => { const a = document.querySelector("#fe-banner a.fe-banner-link"); return a && !document.querySelector("#fe-banner").hidden && a.href.includes("huggingface.co/FineEnvs"); }));
|
| 90 |
-
ok("home: heading says what it explores", /Explore RL environments on Hugging Face/.test(await p.textContent("h1")));
|
| 91 |
-
const kinds = await p.$$eval("[data-kind]", (bs) => bs.map((b) => b.textContent));
|
| 92 |
-
ok("home: Kind filter covers the frameworks", ["Harbor", "Spaces", "Verifiers", "NeMo Gym"].every((k) => kinds.some((x) => x.includes(k))), kinds.join(","));
|
| 93 |
-
ok("home: ORS isn't a Kind of its own (its one Space is with the other Spaces)", !kinds.some((x) => /ORS/.test(x)), kinds.join(","));
|
| 94 |
-
await p.click('[data-kind="openenv"]');
|
| 95 |
-
await p.waitForTimeout(1500); // the server answers (/api/search), after a short debounce
|
| 96 |
-
const firstOE = await p.$$eval("#list .card", (cs) => cs.slice(0, 8).map((c) => c.getAttribute("href")));
|
| 97 |
-
ok("home: OpenEnv Spaces open on FineEnvs' curated servers", firstOE.length === 8 && firstOE.every((h) => h.startsWith("/s/FineEnvs/")), firstOE.join(" "));
|
| 98 |
-
await p.click('[data-kind="verifiers"]');
|
| 99 |
-
await p.waitForTimeout(1500);
|
| 100 |
-
ok("home: filtering to Verifiers shows only Verifiers datasets", await p.$$eval("#list .card .kind-tag", (ts) => ts.length > 0 && ts.every((t) => t.textContent.includes("Verifiers"))));
|
| 101 |
-
await p.click('[data-kind="all"]');
|
| 102 |
-
await p.waitForTimeout(1200);
|
| 103 |
-
await p.fill("#q", "terminal-bench");
|
| 104 |
-
// the server answers the search (/api/search): wait for its answer, not a fixed time (a Space is a round trip away)
|
| 105 |
-
const narrowed = await p.waitForFunction(() => { const cs = [...document.querySelectorAll("#list .card")]; return cs.length > 0 && cs.every((c) => /terminal/i.test(c.textContent)); }, null, { timeout: 15000 }).then(() => true, () => false);
|
| 106 |
-
ok("home: search narrows the list", narrowed);
|
| 107 |
-
await p.fill("#q", "");
|
| 108 |
-
const page1 = await p.textContent("#tr-page");
|
| 109 |
-
await p.click("#tr-next");
|
| 110 |
-
await p.waitForTimeout(200);
|
| 111 |
-
ok("home: trending pages forward", (await p.textContent("#tr-page")) !== page1);
|
| 112 |
-
ok("home: Space cards show the whole picture", await p.$$eval("#list .card.sp .sp-thumb", (is) => is.slice(0, 3).every((i) => Math.abs(i.getBoundingClientRect().width / i.getBoundingClientRect().height - 1200 / 630) < 0.05)));
|
| 113 |
-
await health(p, "home 1440 dark");
|
| 114 |
-
|
| 115 |
-
// a Harbor task's file viewer
|
| 116 |
-
await go(p, `#/t/${HARBOR}/${HTASK}`, 600);
|
| 117 |
-
ok("task: file viewer renders", await until(p, ".fv .fv-row.fv-file"));
|
| 118 |
-
await p.click('.fv-row.fv-file[data-path="tests/test.sh"]');
|
| 119 |
-
await p.waitForTimeout(500);
|
| 120 |
-
ok("task: a shell file is highlighted with line numbers", await p.$$eval(".fv-body .tk-k", (x) => x.length > 2) && await p.$(".fv-body .ln .n"));
|
| 121 |
-
ok("task: the open file is in the URL", (await here(p)).includes("f=tests%2Ftest.sh"));
|
| 122 |
-
await p.click(".fv-body .ln:nth-child(3) .n");
|
| 123 |
-
ok("task: a line can be linked", (await here(p)).includes("L=3"));
|
| 124 |
-
await p.click('[data-act="full"]');
|
| 125 |
-
ok("task: full view covers the page, above the header", await p.evaluate(() => {
|
| 126 |
-
const fv = document.querySelector(".fv.full"); if (!fv) return false;
|
| 127 |
-
const r = fv.getBoundingClientRect(); const hit = document.elementFromPoint(r.left + 40, r.top + 20);
|
| 128 |
-
return fv.contains(hit);
|
| 129 |
-
}));
|
| 130 |
-
await p.keyboard.press("Escape");
|
| 131 |
-
ok("task: Esc closes full view", !(await p.$(".fv.full")));
|
| 132 |
-
const solution = await p.$('.fv-row.fv-file.withheld');
|
| 133 |
-
if (solution) { await solution.click(); await p.waitForTimeout(200); ok("task: withheld files say so", /Not shown/.test(await p.textContent(".fv-body"))); }
|
| 134 |
-
await health(p, "task 1440 dark");
|
| 135 |
-
|
| 136 |
-
// a big Harbor dataset: walked, opens immediately
|
| 137 |
-
await go(p, "#/d/microsoft/ProgramDistill", 2500);
|
| 138 |
-
ok("big dataset: its tasks list without waiting for every file", await until(p, "#list .card, .ix-partial, .tk-card, [data-task]", 30000) || (await p.textContent("#body")).length > 200);
|
| 139 |
-
await health(p, "ProgramDistill 1440");
|
| 140 |
-
|
| 141 |
-
// rows: a generic dataset, paged, its facets filtering on the server
|
| 142 |
-
await go(p, "#/d/FineEnvs/SmolDataEnvs", 600);
|
| 143 |
-
ok("rows: list renders", await until(p, "#list .card", 30000));
|
| 144 |
-
ok("rows: says how it's read", /How it's read/.test(await p.textContent(".rw-how")) && /answer/i.test(await p.textContent(".rw-roles")));
|
| 145 |
-
const firstTitles = await p.$$eval("#list .card .t", (ts) => ts.slice(0, 6).map((t) => t.textContent));
|
| 146 |
-
ok("rows: titles differ from row to row", new Set(firstTitles).size === firstTitles.length, firstTitles.join(" / "));
|
| 147 |
-
const opt = await p.$("#facets .opt");
|
| 148 |
-
if (opt) {
|
| 149 |
-
await opt.click();
|
| 150 |
-
await p.waitForFunction(() => /matching/.test(document.querySelector("#count").textContent) || /available/.test(document.querySelector("#note").textContent), null, { timeout: 60000 }).catch(() => {});
|
| 151 |
-
ok("rows: a facet filters (or says it can't yet)", /matching/.test(await p.textContent("#count")) || /isn't available/.test(await p.textContent("#note")), await p.textContent("#count"));
|
| 152 |
-
}
|
| 153 |
-
await health(p, "rows SmolDataEnvs 1440");
|
| 154 |
-
await go(p, "#/r/FineEnvs/SmolDataEnvs?c=default&s=train&i=3", 600);
|
| 155 |
-
ok("row: an old row link opens its task page", await until(p, ".tp-sec", 30000) && (await here(p)).startsWith("/t/FineEnvs/SmolDataEnvs/train/3"));
|
| 156 |
-
ok("row: the answer is withheld", /Withheld/.test(await p.textContent(".tp-side")) && !(await p.textContent(".tp-body")).includes('"answer"'));
|
| 157 |
-
await p.click('.tp-nav a[title^="Next"]');
|
| 158 |
-
await p.waitForTimeout(2500);
|
| 159 |
-
ok("row: next task navigates", (await here(p)).includes("train/4"));
|
| 160 |
-
await p.keyboard.press("ArrowLeft");
|
| 161 |
-
await p.waitForTimeout(2500);
|
| 162 |
-
ok("row: ← goes back a task", (await here(p)).includes("train/3"));
|
| 163 |
-
await health(p, "row 1440");
|
| 164 |
-
|
| 165 |
-
// a packed Harbor task (TaskTrove)
|
| 166 |
-
await go(p, "#/t/open-thoughts/TaskTrove/train/100", 600);
|
| 167 |
-
ok("packed: unpacked into a file tree", await until(p, ".fv .fv-row.fv-file", 40000));
|
| 168 |
-
ok("packed: its solution is withheld", await p.$('.fv-row.fv-file.withheld[data-path="solution/solve.sh"]'));
|
| 169 |
-
ok("packed: runs on Harbor", /Harbor/.test((await (await p.$("#run-card"))?.textContent()) || ""));
|
| 170 |
-
await health(p, "TaskTrove task 1440");
|
| 171 |
-
|
| 172 |
-
// MiMo: the release, as the MiMo explorer shows it
|
| 173 |
-
await go(p, `#/d/${MIMO}`, 600);
|
| 174 |
-
ok("MiMo: every environment listed", await until(p, "#list .card", 40000) && /7,780/.test(await p.textContent("#count")));
|
| 175 |
-
ok("MiMo: a tile per domain, with its grader", (await p.$$("#tiles .dom")).length === 6 && /Graded by hidden unit tests/.test(await p.textContent("#tiles")));
|
| 176 |
-
await p.click('#tiles .dom[data-tile="Cyber"]');
|
| 177 |
-
await p.waitForTimeout(500);
|
| 178 |
-
ok("MiMo: a domain's tile scopes the facets and the map", /Sanitizer/i.test(await p.textContent("#facets")) && /Cyber, by crash type/.test(await p.textContent("#map-title")));
|
| 179 |
-
await p.fill("#q", "heap overflow png");
|
| 180 |
-
await p.waitForTimeout(600);
|
| 181 |
-
ok("MiMo: search ranks and highlights", /best match/.test(await p.textContent("#order-note")) && (await p.$("#list .card mark")));
|
| 182 |
-
await p.evaluate(() => { document.querySelector("#ov-rewards").open = true; });
|
| 183 |
-
ok("MiMo: the reward design unfolds", /General rubrics, across the dataset/.test(await p.textContent("#ov-rewards")));
|
| 184 |
-
await health(p, "MiMo env 1440");
|
| 185 |
-
await go(p, `#/t/${MIMO}/${MIMO_GENERAL}`, 600);
|
| 186 |
-
ok("MiMo task: its systems and workspace", await until(p, ".tbl-btn", 90000) && (await p.$(".files-grid .file")));
|
| 187 |
-
await p.click(".tbl-btn");
|
| 188 |
-
ok("MiMo task: a table opens with the rows the agent starts with", await until(p, "#modal:not([hidden]) table", 30000));
|
| 189 |
-
await p.click("#modal-close");
|
| 190 |
-
await p.click('.files-grid .file[data-kind="document"]');
|
| 191 |
-
ok("MiMo task: a Word file previews", await until(p, "#modal:not([hidden]) .pv-doc", 40000));
|
| 192 |
-
await p.click("#modal-close");
|
| 193 |
-
ok("MiMo task: the rubric's checks, answers withheld", /checks, weighted/.test(await p.textContent("#sec-grading")) && /pass_anchor/.test(await p.textContent(".tp-side")));
|
| 194 |
-
ok("MiMo task: both runners offered, the MiMo harness first here", /MiMo harness/.test(await p.textContent("#run-runner")));
|
| 195 |
-
ok("MiMo task: links its Harbor twin", await p.$('.tp-links a[href^="/t/FineEnvs/MiMo-V2.6-RL-harbor-general/"]'));
|
| 196 |
-
await health(p, "MiMo task 1440");
|
| 197 |
-
await go(p, `#/r/${MIMO}?c=code&s=train&i=6`, 600);
|
| 198 |
-
ok("MiMo: an old row link opens the task by its id", await until(p, ".tp-sec", 60000) && /^\/t\/XiaomiMiMo\/MiMo-V2\.6-RL-oss\/format-code-task-/.test(await here(p)));
|
| 199 |
-
const twin = await p.$eval('.tp-links a[href^="/t/FineEnvs/MiMo-V2.6-RL-harbor-code/"]', (a) => a.getAttribute("href")).catch(() => null);
|
| 200 |
-
ok("MiMo: links its Harbor twin", !!twin);
|
| 201 |
-
if (twin) {
|
| 202 |
-
await go(p, twin, 600);
|
| 203 |
-
ok("MiMo twin: links back to the release", await until(p, `.tp-links a[href^="/t/${MIMO}/"]`, 40000));
|
| 204 |
-
}
|
| 205 |
-
await go(p, "#/task/music-gK-0768", 600);
|
| 206 |
-
ok("MiMo explorer's addresses still open", await until(p, ".tp-sec", 40000) && (await here(p)) === `/t/${MIMO}/music-gK-0768`);
|
| 207 |
-
await go(p, "#/community?view=tasks", 600);
|
| 208 |
-
ok("community: legacy Tasks links open the rollout feed", await until(p, "#cm-filters select[data-f=runner]", 60000) && !(await p.$("#cm-view")));
|
| 209 |
-
await health(p, "community rollouts 1440");
|
| 210 |
-
|
| 211 |
-
// a live Space: playground, tasks, connect
|
| 212 |
-
await go(p, "#/s/FineEnvs/geoguesser-env", 600);
|
| 213 |
-
ok("space: what it offers fills in", await liveIn(p));
|
| 214 |
-
ok("space: status says running", /Running/.test(await p.textContent("#sp-status")));
|
| 215 |
-
await p.evaluate(() => { const o = document.querySelector(".pg-opts"); if (o) o.open = true; });
|
| 216 |
-
await p.selectOption("#pg-split", "eval");
|
| 217 |
-
await p.fill("#pg-index", "5");
|
| 218 |
-
await p.click("#pg-reset");
|
| 219 |
-
ok("space: a reset starts an episode with an image", await until(p, "#pg-log .rv-img", 40000));
|
| 220 |
-
await p.selectOption("#pg-tool", "look");
|
| 221 |
-
await p.getByRole("spinbutton", { name: "heading_deg", exact: true }).fill("90");
|
| 222 |
-
await p.click("#pg-call");
|
| 223 |
-
await p.waitForTimeout(5000);
|
| 224 |
-
ok("space: a tool call answers in the episode", /Facing 90/.test(await p.textContent("#pg-log")));
|
| 225 |
-
ok("space: tasks list from its Task API", await until(p, "#tk-table [data-task]", 30000));
|
| 226 |
-
await p.click("#tk-table [data-task='1']");
|
| 227 |
-
ok("space: a task opens in full", await until(p, "#tk-detail:not([hidden]) .tk-detail-b"));
|
| 228 |
-
await p.click('[data-agent="cursor"]');
|
| 229 |
-
ok("space: connect shows each agent's config", /mcpServers/.test(await p.textContent("#sp-agent")) && (await p.textContent("#sp-agent")).includes("/mcp/FineEnvs/geoguesser-env"));
|
| 230 |
-
await health(p, "space geoguesser 1440");
|
| 231 |
-
|
| 232 |
-
await go(p, "s/FineEnvs/nayana-ocr-env", 600); // ids and paths hundreds of characters long, no spaces
|
| 233 |
-
if (await liveIn(p)) {
|
| 234 |
-
await p.click("#pg-reset");
|
| 235 |
-
ok("space (long ids): an episode starts", await until(p, "#pg-log .pg-entry", 40000));
|
| 236 |
-
await p.waitForTimeout(800);
|
| 237 |
-
await health(p, "space nayana episode 1440");
|
| 238 |
-
}
|
| 239 |
-
await go(p, "#/s/FineEnvs/latex-ocr-env", 600);
|
| 240 |
-
ok("space (reset/step): fills in", await liveIn(p));
|
| 241 |
-
await p.click("#pg-reset");
|
| 242 |
-
ok("space (reset/step): reset shows the image", await until(p, "#pg-log .rv-img", 40000));
|
| 243 |
-
await p.getByRole("textbox", { name: /^latex$/i }).fill("x^2");
|
| 244 |
-
await p.click("#pg-step");
|
| 245 |
-
ok("space (reset/step): a step earns a reward", await until(p, "#pg-log .pg-entry:last-of-type .rv-reward", 40000));
|
| 246 |
-
await health(p, "space latex 1440");
|
| 247 |
-
|
| 248 |
-
// a NeMo Gym server: its own routes, with the session's cookie carrying the game
|
| 249 |
-
await go(p, "#/s/FineEnvs/wordle-nemo-gym", 600);
|
| 250 |
-
if (await until(p, "#pg-route", 40000)) {
|
| 251 |
-
ok("nemo gym: labelled as such", /NeMo Gym/.test(await p.textContent("main")));
|
| 252 |
-
await p.selectOption("#pg-route", { label: "POST /seed_session" });
|
| 253 |
-
await p.click("#pg-send");
|
| 254 |
-
await p.waitForTimeout(2500);
|
| 255 |
-
await p.selectOption("#pg-route", { label: "POST /guess" });
|
| 256 |
-
ok("nemo gym: a short string is a one-line input", await p.$('input[data-k="word"]'));
|
| 257 |
-
await p.getByRole("textbox", { name: "Word", exact: true }).fill("crane");
|
| 258 |
-
await p.click("#pg-send");
|
| 259 |
-
await p.waitForTimeout(2500);
|
| 260 |
-
await p.selectOption("#pg-route", { label: "POST /get_history" });
|
| 261 |
-
await p.click("#pg-send");
|
| 262 |
-
await p.waitForTimeout(2500);
|
| 263 |
-
ok("nemo gym: the session keeps the game between calls", /Guess 1: crane/.test(await p.textContent("#pg-log")));
|
| 264 |
-
} else ok("nemo gym: routes playground renders", false, "no #pg-route (asleep?)");
|
| 265 |
-
await health(p, "nemo gym space 1440");
|
| 266 |
-
|
| 267 |
-
await go(p, "#/s/AdithyaSK/openenv-harbor-ui-test", 600);
|
| 268 |
-
if (await until(p, "#sec-interface .panel-b", 40000) && (await p.$("#sec-harbor"))) {
|
| 269 |
-
ok("harbor space: maps its dataset back to the explorer", await p.$('#sec-harbor a[href="/d/FineEnvs/MiMo-V2.6-RL-harbor-terminal"]'));
|
| 270 |
-
}
|
| 271 |
-
await health(p, "harbor space 1440");
|
| 272 |
-
await p.context().close();
|
| 273 |
-
}
|
| 274 |
-
|
| 275 |
-
// ── the frameworks' rows, and a model-graded task ────────────────────────────
|
| 276 |
-
{
|
| 277 |
-
const p = await page(1440, "dark");
|
| 278 |
-
await go(p, "t/nvidia/Nemotron-RL-Agentic-Function-Calling-Pivot-v1/train/0", 600);
|
| 279 |
-
ok("NeMo Gym: the row reads as a conversation, not a table", await until(p, ".tp-sec", 40000)
|
| 280 |
-
&& await p.evaluate(() => { const t = document.querySelector(".tp-sec"); const turns = t ? [...t.querySelectorAll(".rl-turn")] : [];
|
| 281 |
-
return turns.length > 1 && turns.every((x) => !x.closest("table")) && turns.some((x) => /user/i.test(x.querySelector(".rl-who")?.textContent || "")); }));
|
| 282 |
-
// (a tool's output may be shown as a table: data, inside its turn; the turns themselves never are)
|
| 283 |
-
ok("NeMo Gym: its title is the request, not a row number", !/^(Row )?\d+$/.test((await p.textContent("h1")).trim()));
|
| 284 |
-
ok("NeMo Gym: the expert's next action is withheld", /expected_action/.test(await p.textContent(".tp-side")) && !(await p.textContent(".tp-body")).includes('"expected_action"'));
|
| 285 |
-
ok("NeMo Gym: says how to run it with NeMo Gym", /Run it with NeMo Gym/.test(await p.textContent("main")));
|
| 286 |
-
await health(p, "NeMo Gym row 1440");
|
| 287 |
-
await go(p, "t/sungyub/deepscaler-preview-verl/train/0", 600);
|
| 288 |
-
ok("verl: the ground truth is withheld", await until(p, ".tp-sec", 40000) && /ground_truth/.test(await p.textContent(".tp-side")));
|
| 289 |
-
ok("verl: its math is typeset (KaTeX), not left as TeX", await until(p, "h1 .math .katex", 20000) && !(await p.textContent("h1")).includes("\\frac"));
|
| 290 |
-
await health(p, "verl row 1440");
|
| 291 |
-
await go(p, "t/FineEnvs/MiMo-V2.6-RL-harbor-general/tasks/s3k_0052_accounting_audit_tax_en_t2_rl_008", 600);
|
| 292 |
-
// signed in, the run panel offers the judge; signed out (a Space without a session), the page still says how it's graded
|
| 293 |
-
ok("judge: a model-graded task offers a judge, through the relay", await until(p, "#run-card", 30000) && (
|
| 294 |
-
(/Judge model/.test(await p.textContent("#run-card")) && /relay/.test(await p.textContent("#run-card")))
|
| 295 |
-
|| (/Sign in/i.test(await p.textContent("#run-card")) && /Graded by a model/.test(await p.textContent("main")))));
|
| 296 |
-
await health(p, "model-graded task 1440");
|
| 297 |
-
await p.context().close();
|
| 298 |
-
}
|
| 299 |
-
|
| 300 |
-
// ── every page, every width, both themes ─────────────────────────────────────
|
| 301 |
-
const PAGES = [["home", "#/"], ["dataset", `#/d/${HARBOR}`], ["task", `#/t/${HARBOR}/${HTASK}`], ["rows", "#/d/FineEnvs/SmolDataEnvs"],
|
| 302 |
-
["row", "#/t/FineEnvs/SmolDataEnvs/train/2"], ["packed", "#/t/open-thoughts/TaskTrove/train/100"],
|
| 303 |
-
["space", "#/s/FineEnvs/geoguesser-env"], ["traces", "#/t/open-thoughts/AgentTrove/train/0"], ["mimo", `#/d/${MIMO}`],
|
| 304 |
-
["mimo-task", `#/t/${MIMO}/${MIMO_GENERAL}`], ["mimo-code", `#/t/${MIMO}/format-code-task-000720`], ["community", "#/community"]];
|
| 305 |
-
for (const [w, theme] of [[375, "light"], [768, "dark"], [1280, "light"], [1680, "dark"]]) {
|
| 306 |
-
const p = await page(w, theme);
|
| 307 |
-
for (const [name, hash] of PAGES) {
|
| 308 |
-
await go(p, hash, 3500);
|
| 309 |
-
await p.waitForTimeout(name === "space" ? 6000 : 1500);
|
| 310 |
-
await p.screenshot({ path: `${SHOTS}/${name}-${w}-${theme}.png`, fullPage: false });
|
| 311 |
-
await health(p, `${name} ${w} ${theme}`);
|
| 312 |
-
}
|
| 313 |
-
await p.context().close();
|
| 314 |
-
}
|
| 315 |
-
|
| 316 |
-
await browser.close();
|
| 317 |
-
const failed = results.filter((r) => !r.pass);
|
| 318 |
-
console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
|
| 319 |
-
for (const f of failed) console.log(`FAIL ${f.name}${f.detail ? ` (${f.detail})` : ""}`);
|
| 320 |
-
process.exit(failed.length ? 1 : 0);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web-admin/admin.css
DELETED
|
@@ -1,175 +0,0 @@
|
|
| 1 |
-
/* The admin dashboard, on top of the explorer's app.css and rlx.css. */
|
| 2 |
-
/* admin */
|
| 3 |
-
.ad-tabs { flex-wrap: wrap; }
|
| 4 |
-
.ad-stats { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; }
|
| 5 |
-
.ad-stat { background: var(--surface); border: 1px solid var(--border); border-radius: var(--r); padding: 12px 14px; display: grid; gap: 1px; min-width: 0; }
|
| 6 |
-
.ad-stat b { font-size: 20px; font-weight: 650; letter-spacing: -.02em; font-variant-numeric: tabular-nums; }
|
| 7 |
-
.ad-stat span { font-size: 12.5px; color: var(--muted); }
|
| 8 |
-
.ad-stat em { font-style: normal; font-size: 12px; color: var(--faint); }
|
| 9 |
-
.ad-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); gap: 16px; margin-top: 16px; }
|
| 10 |
-
.ad-bars { display: grid; grid-template-columns: repeat(14, minmax(0, 1fr)); gap: 4px; height: 120px; align-items: end; }
|
| 11 |
-
.ad-bars div { display: grid; grid-template-rows: 1fr auto; height: 100%; align-items: end; gap: 3px; }
|
| 12 |
-
.ad-bars i { display: block; border-radius: 4px 4px 0 0; background: var(--focus); opacity: .8; min-height: 2px; }
|
| 13 |
-
.ad-bars span { font-size: 10.5px; color: var(--faint); text-align: center; font-variant-numeric: tabular-nums; }
|
| 14 |
-
.ad-new { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
|
| 15 |
-
.ad-new li { display: flex; justify-content: space-between; gap: 10px; font-size: 13px; }
|
| 16 |
-
.ad-new a { display: inline-flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; }
|
| 17 |
-
.ad-new code { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 18 |
-
.ad-runs { display: grid; gap: 0; }
|
| 19 |
-
.ad-run { display: grid; grid-template-columns: 130px minmax(0, 1fr) 140px 200px 60px 90px auto; gap: 12px; align-items: center; padding: 9px 0; border-top: 1px solid var(--border); font-size: 13px; }
|
| 20 |
-
.ad-run:first-child, .ad-runs > p + .ad-run { border-top: 0; }
|
| 21 |
-
.ad-task { display: grid; min-width: 0; } .ad-task b { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 22 |
-
.ad-task em, .ad-when em { font-style: normal; font-size: 12px; color: var(--faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
|
| 23 |
-
.ad-user, .ad-model { color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-flex; align-items: center; gap: 5px; }
|
| 24 |
-
.ad-when { font-size: 12.5px; color: var(--faint); text-align: right; }
|
| 25 |
-
.ad-act { display: flex; gap: 6px; justify-content: flex-end; flex-wrap: wrap; }
|
| 26 |
-
.ad-envs { display: grid; }
|
| 27 |
-
.ad-env { display: grid; grid-template-columns: minmax(0, 1fr) 170px 80px 90px 70px 70px 150px; gap: 10px; align-items: center; padding: 8px 14px; border-top: 1px solid var(--border); font-size: 13px; }
|
| 28 |
-
.ad-env.head { border-top: 0; background: var(--surface-2); font-size: 11.5px; font-weight: 600; color: var(--faint); text-transform: uppercase; letter-spacing: .04em; }
|
| 29 |
-
.ad-env.gone { opacity: .55; }
|
| 30 |
-
.ad-env .num { text-align: right; font-variant-numeric: tabular-nums; }
|
| 31 |
-
.ad-name { display: flex; align-items: center; gap: 6px; min-width: 0; flex-wrap: wrap; }
|
| 32 |
-
.ad-name a { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; }
|
| 33 |
-
.ad-name code { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 34 |
-
.hf-select-mimo.sm { height: 28px; font-size: 12.5px; width: 100%; }
|
| 35 |
-
.ad-cols { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 460px), 1fr)); gap: 12px; }
|
| 36 |
-
.ad-form { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 14px; }
|
| 37 |
-
.ad-form .wide { grid-column: 1 / -1; }
|
| 38 |
-
.ad-form textarea { height: auto; padding: 8px 10px; resize: vertical; font-size: 12.5px; line-height: 1.5; }
|
| 39 |
-
.ad-checks { display: flex; flex-wrap: wrap; gap: 8px 18px; font-size: 13.5px; }
|
| 40 |
-
.ad-checks label, .ad-switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
|
| 41 |
-
.ad-switch span { display: grid; } .ad-switch em { font-style: normal; font-size: 12.5px; color: var(--faint); }
|
| 42 |
-
.ad-audit { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; font-size: 13px; }
|
| 43 |
-
.ad-audit li { display: grid; grid-template-columns: 110px 140px minmax(0, 1fr); gap: 10px; }
|
| 44 |
-
.ic.up { transform: rotate(180deg); }
|
| 45 |
-
@media (max-width: 1100px) { .ad-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); } .ad-grid { grid-template-columns: minmax(0, 1fr); }
|
| 46 |
-
.ad-run { grid-template-columns: 120px minmax(0, 1fr) 60px auto; } .ad-run .ad-user, .ad-run .ad-model, .ad-run .ad-when { display: none; }
|
| 47 |
-
.ad-env { grid-template-columns: minmax(0, 1fr) 140px 120px; } .ad-env > .num { display: none; } }
|
| 48 |
-
@media (max-width: 760px) { .ad-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } .ad-form { grid-template-columns: minmax(0, 1fr); }
|
| 49 |
-
.ad-env { grid-template-columns: minmax(0, 1fr); } .ad-env.head { display: none; } .ad-act { justify-content: flex-start; }
|
| 50 |
-
.ad-run { grid-template-columns: minmax(0, 1fr) auto; } .ad-run > span:first-child { grid-column: 1 / -1; } .ad-audit li { grid-template-columns: minmax(0, 1fr); gap: 0; } }
|
| 51 |
-
|
| 52 |
-
/* layout: a section list on the left, the section on the right */
|
| 53 |
-
.ad-shell { display: grid; grid-template-columns: 210px minmax(0, 1fr); gap: 28px; padding-top: 22px; padding-bottom: 64px; align-items: start; }
|
| 54 |
-
.ad-nav { position: sticky; top: calc(var(--header-h) + 18px); display: grid; gap: 2px; }
|
| 55 |
-
.ad-nav a { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px; color: var(--muted); font-weight: 500; font-size: 13.5px; }
|
| 56 |
-
.ad-nav a:hover { background: var(--surface-2); color: var(--text); }
|
| 57 |
-
.ad-nav a.on { background: var(--surface-2); color: var(--text); box-shadow: inset 2px 0 0 var(--text); }
|
| 58 |
-
.ad-nav a .ic { color: var(--faint); }
|
| 59 |
-
.ad-nav a.on .ic { color: var(--text); }
|
| 60 |
-
.ad-nav em { margin-left: auto; font-style: normal; font-size: 11.5px; font-weight: 600; color: var(--muted); background: var(--surface-3); border-radius: 99px; padding: 1px 7px; }
|
| 61 |
-
.ad-nav em.live { color: var(--live); background: color-mix(in srgb, var(--live) 14%, transparent); }
|
| 62 |
-
.ad-main { min-width: 0; }
|
| 63 |
-
.ad-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px 20px; flex-wrap: wrap; margin-bottom: 16px; }
|
| 64 |
-
.ad-head h1 { font-size: 22px; letter-spacing: -.02em; }
|
| 65 |
-
.ad-head p { color: var(--muted); font-size: 13.5px; margin-top: 4px; max-width: 720px; }
|
| 66 |
-
.ad-actions { display: flex; gap: 8px; flex-wrap: wrap; }
|
| 67 |
-
.ad-tools { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 12px; }
|
| 68 |
-
.ad-tools .search { flex: 1 1 260px; }
|
| 69 |
-
.ad-tools .seg { flex-wrap: wrap; }
|
| 70 |
-
|
| 71 |
-
/* overview */
|
| 72 |
-
.ad-hello { display: flex; align-items: center; gap: 14px; margin-bottom: 18px; flex-wrap: wrap; }
|
| 73 |
-
.ad-hello h1 { font-size: 22px; letter-spacing: -.02em; }
|
| 74 |
-
.ad-hello p { color: var(--muted); font-size: 13.5px; display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
|
| 75 |
-
.ad-hello .ad-actions { margin-left: auto; }
|
| 76 |
-
.ad-stat { display: flex; gap: 12px; align-items: flex-start; }
|
| 77 |
-
.ad-stat .ic-wrap { width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; background: var(--surface-2); color: var(--muted); flex: none; }
|
| 78 |
-
.ad-stat > div { display: grid; gap: 1px; min-width: 0; }
|
| 79 |
-
.ad-stat.live .ic-wrap { color: var(--live); background: color-mix(in srgb, var(--live) 12%, transparent); }
|
| 80 |
-
.ad-stat.warn .ic-wrap { color: var(--warn); background: color-mix(in srgb, var(--warn) 12%, transparent); }
|
| 81 |
-
.ad-live { margin-top: 16px; border-color: color-mix(in srgb, var(--live) 35%, var(--border)); }
|
| 82 |
-
.ad-models { list-style: none; margin: 0; padding: 0; display: grid; gap: 7px; font-size: 13px; }
|
| 83 |
-
.ad-models li { display: grid; grid-template-columns: minmax(0, 1fr) 90px 30px; gap: 10px; align-items: center; }
|
| 84 |
-
.ad-models code { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
| 85 |
-
.ad-models b { text-align: right; font-variant-numeric: tabular-nums; }
|
| 86 |
-
.ad-new li { display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 10px; align-items: center; }
|
| 87 |
-
.ad-new a { display: grid; min-width: 0; }
|
| 88 |
-
.ad-new a em { font-style: normal; font-size: 12px; color: var(--faint); }
|
| 89 |
-
.ad-th { width: 40px; height: 30px; border-radius: 6px; object-fit: cover; border: 1px solid var(--border); background: var(--surface-2); flex: none; }
|
| 90 |
-
.ad-th.ic-only { display: grid; place-items: center; color: var(--faint); }
|
| 91 |
-
.ad-env .ad-th { width: 34px; height: 26px; }
|
| 92 |
-
|
| 93 |
-
/* people */
|
| 94 |
-
.pfp { border-radius: 50%; flex: none; background: var(--surface-3); object-fit: cover; }
|
| 95 |
-
.pfp.none { display: inline-grid; place-items: center; font-size: 11px; font-weight: 700; color: var(--muted); }
|
| 96 |
-
.person { display: inline-flex; align-items: center; gap: 7px; min-width: 0; font-size: 13px; }
|
| 97 |
-
.person a, .person .link, .person b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); font-weight: 500; }
|
| 98 |
-
.person .link:hover, .person a:hover { text-decoration: underline; }
|
| 99 |
-
.ad-people { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr)); gap: 12px; }
|
| 100 |
-
.ad-person { padding: 16px; display: grid; gap: 14px; align-content: start; }
|
| 101 |
-
.ad-person header { display: flex; gap: 12px; align-items: center; }
|
| 102 |
-
.ad-person header > div { display: grid; min-width: 0; }
|
| 103 |
-
.ad-person header a { display: inline-flex; align-items: center; gap: 4px; color: var(--text); }
|
| 104 |
-
.ad-person header em { font-style: normal; font-size: 12.5px; color: var(--faint); }
|
| 105 |
-
.live-txt { color: var(--live); }
|
| 106 |
-
.ad-pstats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; }
|
| 107 |
-
.ad-pstats div { background: var(--surface-2); border-radius: 8px; padding: 7px 8px; display: grid; }
|
| 108 |
-
.ad-pstats b { font-size: 15px; font-variant-numeric: tabular-nums; }
|
| 109 |
-
.ad-pstats span { font-size: 11.5px; color: var(--faint); }
|
| 110 |
-
.ad-person .btn { justify-self: start; }
|
| 111 |
-
.ad-run.is-live { background: color-mix(in srgb, var(--live) 5%, transparent); }
|
| 112 |
-
.ad-model { display: grid; min-width: 0; }
|
| 113 |
-
.ad-model em { font-style: normal; font-size: 12px; color: var(--faint); }
|
| 114 |
-
.chip.new { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%, transparent); }
|
| 115 |
-
|
| 116 |
-
/* collections, settings */
|
| 117 |
-
.ad-swatch { width: 24px; height: 24px; border-radius: 7px; display: inline-grid; place-items: center; color: #fff; background: var(--dc); }
|
| 118 |
-
.ad-swatch .ic { color: #fff !important; }
|
| 119 |
-
.ad-settings { display: grid; gap: 0; padding: 4px 20px; max-width: 820px; }
|
| 120 |
-
.ad-set { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 0; border-bottom: 1px solid var(--border); flex-wrap: wrap; }
|
| 121 |
-
.ad-set.col { display: grid; gap: 10px; justify-content: stretch; }
|
| 122 |
-
.ad-set b { display: block; font-size: 14px; }
|
| 123 |
-
.ad-set span { font-size: 12.5px; color: var(--faint); }
|
| 124 |
-
.ad-nums { display: flex; align-items: center; gap: 8px; }
|
| 125 |
-
.ad-nums .input { width: 80px; }
|
| 126 |
-
.ad-save-row { display: flex; align-items: center; justify-content: flex-end; gap: 12px; padding: 16px 0; }
|
| 127 |
-
.ad-preview { border-radius: 8px; }
|
| 128 |
-
.toggle { position: relative; display: inline-block; width: 42px; height: 24px; flex: none; cursor: pointer; }
|
| 129 |
-
.toggle input { position: absolute; opacity: 0; width: 100%; height: 100%; margin: 0; cursor: pointer; }
|
| 130 |
-
.toggle i { position: absolute; inset: 0; border-radius: 99px; background: var(--surface-3); transition: background .15s; }
|
| 131 |
-
.toggle i::after { content: ""; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: var(--shadow-sm); transition: transform .15s; }
|
| 132 |
-
.toggle input:checked + i { background: var(--ok); }
|
| 133 |
-
.toggle input:checked + i::after { transform: translateX(18px); }
|
| 134 |
-
.toggle input:focus-visible + i { box-shadow: 0 0 0 3px color-mix(in srgb, var(--focus) 30%, transparent); }
|
| 135 |
-
.ad-audit li { grid-template-columns: 170px minmax(0, 1fr) 90px; align-items: center; }
|
| 136 |
-
@media (max-width: 1000px) {
|
| 137 |
-
.ad-shell { grid-template-columns: minmax(0, 1fr); gap: 14px; }
|
| 138 |
-
.ad-nav { position: static; display: flex; overflow-x: auto; gap: 4px; scrollbar-width: none; margin: 0 calc(-1 * var(--gutter)); padding: 0 var(--gutter); }
|
| 139 |
-
.ad-nav a { flex: none; } .ad-nav a.on { box-shadow: inset 0 -2px 0 var(--text); border-radius: 8px 8px 0 0; }
|
| 140 |
-
}
|
| 141 |
-
@media (max-width: 760px) { .ad-hello .ad-actions { margin-left: 0; } .ad-pstats { grid-template-columns: repeat(2, minmax(0, 1fr)); } .ad-audit li { grid-template-columns: minmax(0, 1fr); } }
|
| 142 |
-
|
| 143 |
-
/* Evidence and maintenance are separate from dataset indexes and saved runs. */
|
| 144 |
-
.ad-operations { margin: 16px 0; }
|
| 145 |
-
.ad-ops-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px 24px; }
|
| 146 |
-
.ad-ops-grid > div { min-width: 0; }
|
| 147 |
-
.ad-ops-grid p { margin: 6px 0 10px; overflow-wrap: anywhere; }
|
| 148 |
-
.ad-ops-grid dd, .ad-ops-grid dt { min-width: 0; overflow-wrap: anywhere; }
|
| 149 |
-
.ad-check-states, .ad-runner-summary { display: flex; flex-wrap: wrap; gap: 6px; }
|
| 150 |
-
.ad-check-states, .ad-ops-note { grid-column: 1 / -1; }
|
| 151 |
-
.ad-check-states .chip { max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
|
| 152 |
-
.ad-ops-note summary { cursor: pointer; margin-bottom: 10px; }
|
| 153 |
-
.ad-tools .input, .ad-tools select { min-width: 0; max-width: 100%; width: auto; }
|
| 154 |
-
.ad-tools .search { min-width: 0; }
|
| 155 |
-
.ad-name > .chip { max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
| 156 |
-
.ad-stat b { overflow-wrap: anywhere; }
|
| 157 |
-
.ad-task, .ad-model, .ad-when, .ad-act { min-width: 0; }
|
| 158 |
-
.ad-main .panel { min-width: 0; }
|
| 159 |
-
.ad-main .tbl { overflow-x: auto; max-width: 100%; }
|
| 160 |
-
.ad-main #ad-run-error { margin-bottom: 10px; color: var(--muted); font-size: 13px; }
|
| 161 |
-
@media (max-width: 760px) {
|
| 162 |
-
.ad-ops-grid { grid-template-columns: minmax(0, 1fr); }
|
| 163 |
-
.ad-hello h1 { overflow-wrap: anywhere; }
|
| 164 |
-
.ad-stat { padding: 10px; gap: 8px; }
|
| 165 |
-
.ad-stat .ic-wrap { display: none; }
|
| 166 |
-
.ad-tools .search { flex-basis: 100%; }
|
| 167 |
-
.ad-tools > select { flex: 1 1 130px; }
|
| 168 |
-
.ad-head .ad-actions { width: 100%; }
|
| 169 |
-
.ad-run { gap: 8px; }
|
| 170 |
-
.ad-run .ad-act { grid-column: 1 / -1; }
|
| 171 |
-
}
|
| 172 |
-
|
| 173 |
-
@media (min-width: 1101px) and (max-width: 1450px) { .ad-stats { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
| 174 |
-
|
| 175 |
-
.ad-name > .faint { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web-admin/admin.js
DELETED
|
@@ -1,406 +0,0 @@
|
|
| 1 |
-
// The admin dashboard, on its own private Space, for FineEnvs members: what's happening (rollouts, people, new
|
| 2 |
-
// environments), and the controls (what trends at the top, what's hidden, the collections, indexing, the rollout
|
| 3 |
-
// switches). The server checks membership on every call. Links to rollouts, datasets and Spaces open the explorer.
|
| 4 |
-
import { $, api, esc, fmt, ago, dur, sk, emptyState, toast, statusPill, rewardBadge, spinner } from "/js/util.js";
|
| 5 |
-
import { icon } from "/js/icons.js";
|
| 6 |
-
import { getSession } from "/js/session.js";
|
| 7 |
-
|
| 8 |
-
const TABS = [["overview", "Overview", "grid"], ["rollouts", "Rollouts", "play"], ["people", "People", "users"], ["environments", "Environments", "database"],
|
| 9 |
-
["collections", "Collections", "layout"], ["indexes", "Indexing & API checks", "archive"], ["settings", "Settings", "gauge"], ["audit", "Audit log", "list"]];
|
| 10 |
-
const enc = (s) => s.split("/").map(encodeURIComponent).join("/");
|
| 11 |
-
const compact = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 });
|
| 12 |
-
const bytes = (n) => (n == null ? "–" : n > 1e6 ? `${(n / 1e6).toFixed(1)} MB` : `${Math.round(n / 1e3)} KB`);
|
| 13 |
-
let EXPLORER = ""; // the public explorer: datasets, Spaces and rollouts open there
|
| 14 |
-
const envHref = (e) => `${EXPLORER}/${e.kind === "space" ? "s" : "d"}/${enc(e.id)}`;
|
| 15 |
-
const runHref = (id) => `${EXPLORER}/run/${encodeURIComponent(id)}`;
|
| 16 |
-
const thumb = (id) => `https://cdn-thumbnails.huggingface.co/social-thumbnails/spaces/${enc(id)}.png`;
|
| 17 |
-
let alive = true, timer = null, counts = {}, events = null;
|
| 18 |
-
const runnerName = v => ({harbor:"Harbor", mimo:"MiMo", "nemo-gym":"NeMo Gym", openenv:"OpenEnv", verifiers:"Verifiers", verl:"verl", skyrl:"SkyRL"}[v] || v || "Unknown");
|
| 19 |
-
export function unmount() { alive = false; clearTimeout(timer); events?.abort(); }
|
| 20 |
-
|
| 21 |
-
// a person: their Hub picture (or their initial), their name, a link to their profile
|
| 22 |
-
const pfp = (name, src, size = 22) => (src ? `<img class="pfp" src="${esc(src)}" alt="" width="${size}" height="${size}" loading="lazy">`
|
| 23 |
-
: `<span class="pfp none" style="width:${size}px;height:${size}px">${esc((name || "?").slice(0, 1).toUpperCase())}</span>`);
|
| 24 |
-
const person = (name, src, { link = true, filter = false } = {}) => `<span class="person">${pfp(name, src)}${filter
|
| 25 |
-
? `<button class="link" data-user="${esc(name)}" title="Only ${esc(name)}'s rollouts">${esc(name)}</button>`
|
| 26 |
-
: link ? `<a href="https://huggingface.co/${encodeURIComponent(name)}" target="_blank" rel="noopener">${esc(name)}</a>` : `<b>${esc(name)}</b>`}</span>`;
|
| 27 |
-
|
| 28 |
-
export async function mount(el, { qs, explorer }) {
|
| 29 |
-
alive = true;
|
| 30 |
-
events?.abort(); events = new AbortController();
|
| 31 |
-
EXPLORER = explorer || "";
|
| 32 |
-
const s = getSession();
|
| 33 |
-
if (!s.user || !s.is_admin) {
|
| 34 |
-
el.innerHTML = `<div class="wrap page">${emptyState("shield", "Admins only", s.user ? `The admin dashboard is for members of the ${esc(s.admin_org)} organization on Hugging Face.`
|
| 35 |
-
: `Sign in with a ${esc(s.admin_org || "FineEnvs")} account.`, s.user ? "" : `<a class="btn primary" href="/login">${icon("user", 15)}Sign in with Hugging Face</a>`)}</div>`;
|
| 36 |
-
return;
|
| 37 |
-
}
|
| 38 |
-
let tab = TABS.some(([k]) => k === qs.get("tab")) ? qs.get("tab") : "overview";
|
| 39 |
-
const preset = { user: qs.get("user") || "" };
|
| 40 |
-
el.innerHTML = `<div class="wrap ad-shell fade-in">
|
| 41 |
-
<nav class="ad-nav" id="ad-nav" aria-label="Admin sections"></nav>
|
| 42 |
-
<section class="ad-main"><div id="ad-body"></div></section></div>`;
|
| 43 |
-
const nav = () => {
|
| 44 |
-
$("#ad-nav", el).innerHTML = TABS.map(([k, l, ic]) => `<a href="#/${k === "overview" ? "" : `?tab=${k}`}" data-tab="${k}" class="${k === tab ? "on" : ""}">
|
| 45 |
-
${icon(ic, 15)}<span>${l}</span>${counts[k] ? `<em class="${k === "rollouts" ? "live" : ""}">${counts[k]}</em>` : ""}</a>`).join("");
|
| 46 |
-
};
|
| 47 |
-
const show = () => {
|
| 48 |
-
clearTimeout(timer);
|
| 49 |
-
nav();
|
| 50 |
-
history.replaceState(null, "", `#/${tab === "overview" ? "" : `?tab=${tab}${tab === "rollouts" && preset.user ? `&user=${encodeURIComponent(preset.user)}` : ""}`}`);
|
| 51 |
-
$("#ad-body", el).replaceWith(Object.assign(document.createElement("div"), {id:"ad-body"}));
|
| 52 |
-
const body = $("#ad-body", el);
|
| 53 |
-
body.innerHTML = `<div class="panel"><div class="panel-b">${sk.lines(80, 60, 90, 70)}</div></div>`;
|
| 54 |
-
({ overview, rollouts, people, environments, collections, indexes, settings, audit })[tab](body, { go, preset, s }).catch((e) => {
|
| 55 |
-
if (alive && body.isConnected) body.innerHTML = `<div class="note-box err">${icon("alert")}<span>${esc(e.message)}</span></div>`;
|
| 56 |
-
});
|
| 57 |
-
window.scrollTo(0, 0);
|
| 58 |
-
};
|
| 59 |
-
const go = (t, opts = {}) => { tab = t; Object.assign(preset, opts); show(); };
|
| 60 |
-
el.addEventListener("click", (e) => {
|
| 61 |
-
const t = e.target.closest("[data-tab]");
|
| 62 |
-
if (t) { e.preventDefault(); preset.user = ""; go(t.dataset.tab); }
|
| 63 |
-
const u = e.target.closest("[data-user]");
|
| 64 |
-
if (u) { e.preventDefault(); go("rollouts", { user: u.dataset.user }); }
|
| 65 |
-
}, {signal:events.signal});
|
| 66 |
-
show();
|
| 67 |
-
// live counts beside the sections: running rollouts, new environments
|
| 68 |
-
api("/api/admin/overview").then((o) => { counts = { rollouts: o.rollouts.live || "", environments: o.new_count || "" }; if (alive) nav(); }).catch(() => {});
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
const head = (title, sub, actions = "") => `<div class="ad-head"><div><h1>${title}</h1>${sub ? `<p>${sub}</p>` : ""}</div><div class="ad-actions">${actions}</div></div>`;
|
| 72 |
-
const stat = (ic, n, label, sub = "", tone = "") => `<div class="ad-stat ${tone}"><span class="ic-wrap">${icon(ic, 16)}</span><div><b>${n}</b><span>${label}</span>${sub ? `<em>${sub}</em>` : ""}</div></div>`;
|
| 73 |
-
const runRow = (r, actions = false) => `<div class="ad-run${r.live ? " is-live" : ""}">
|
| 74 |
-
<span>${statusPill(r.status)}</span>
|
| 75 |
-
<a class="ad-task" href="${runHref(r.id)}" target="_blank" rel="noopener"><b title="${esc(r.title || r.path)}">${esc(r.title || r.path)}</b><em title="${esc(r.dataset)}">${esc(r.dataset)} · ${esc(runnerName(r.runner))}</em><em>${r.community ? "On Community" : r.moderated ? "Moderated" : r.visibility === "public" ? "Public · not eligible for Community" : "Private"}</em></a>
|
| 76 |
-
${person(r.user, r.avatar, { filter: true })}
|
| 77 |
-
<span class="ad-model" title="${esc(r.model)}">${esc((r.model || "").split("/")[1] || r.model || "endpoint")}<em>${esc(r.harness || runnerName(r.runner))}</em></span>
|
| 78 |
-
<span>${rewardBadge(r.reward, r.status)}</span>
|
| 79 |
-
<span class="ad-when">${ago(r.created_at)}<em>${dur(r.wall_s || ((r.finished_at || Date.now() / 1000) - (r.started_at || r.created_at)))}</em></span>
|
| 80 |
-
${actions ? `<span class="ad-act">${r.live ? `<button class="btn sm danger" data-cancel="${esc(r.id)}">${icon("stop", 12)}Stop</button>` : ""}
|
| 81 |
-
${r.moderated ? `<button class="btn sm" data-unhide="${esc(r.id)}">Remove moderation</button>` : r.community ? `<button class="btn sm ghost" data-hide="${esc(r.id)}">Hide from Community</button>` : ""}</span>` : ""}</div>`;
|
| 82 |
-
|
| 83 |
-
async function runActions(body, reload) {
|
| 84 |
-
body.runEvents?.abort(); body.runEvents = new AbortController();
|
| 85 |
-
body.addEventListener("click", async (e) => {
|
| 86 |
-
const c = e.target.closest("[data-cancel]"), h = e.target.closest("[data-hide]"), u = e.target.closest("[data-unhide]");
|
| 87 |
-
try {
|
| 88 |
-
if (c) { if (!confirm("Stop this rollout? The sandbox shuts down and it isn't graded.")) return;
|
| 89 |
-
await api(`/api/admin/rollouts/${c.dataset.cancel}/cancel`, { method: "POST" }); toast("Asked the explorer to stop it: a few seconds"); }
|
| 90 |
-
else if (h) { if (!confirm("Take this rollout off Community? Its owner still sees it.")) return;
|
| 91 |
-
await api(`/api/admin/rollouts/${h.dataset.hide}/moderate`, { method: "POST", body: { hidden: true } }); toast("Hidden from Community"); }
|
| 92 |
-
else if (u) { await api(`/api/admin/rollouts/${u.dataset.unhide}/moderate`, { method: "POST", body: { hidden: false } }); toast("Moderation removed; Community eligibility still applies"); }
|
| 93 |
-
else return;
|
| 94 |
-
reload();
|
| 95 |
-
} catch (err) { toast(err.message); }
|
| 96 |
-
}, {signal:body.runEvents.signal});
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
// ── overview ─────────────────────────────────────────────────────────────────
|
| 100 |
-
async function overview(body, { go, s }) {
|
| 101 |
-
const o = await api("/api/admin/overview");
|
| 102 |
-
if (!alive || !body.isConnected) return;
|
| 103 |
-
const r = o.rollouts, e = o.environments, on = o.settings.rollouts_enabled;
|
| 104 |
-
const peak = Math.max(1, ...r.by_day.map(([, n]) => n));
|
| 105 |
-
const live = o.live;
|
| 106 |
-
const last = o.recent[0];
|
| 107 |
-
body.innerHTML = `
|
| 108 |
-
<div class="ad-hello">${pfp(s.user.name, s.user.avatar, 44)}<div><h1>Hi, ${esc(s.user.name)}</h1>
|
| 109 |
-
<p><span class="dot" style="--dc:${on ? "var(--ok)" : "var(--warn)"}"></span> Rollouts are ${on ? "on" : "<b>paused</b>"} · ${fmt.format(r.live)} running now${last ? ` · last one ${ago(last.created_at)}` : ""}</p></div>
|
| 110 |
-
<div class="ad-actions"><button class="btn ${on ? "" : "primary"}" id="ov-toggle" type="button">${icon(on ? "stop" : "play", 14)}${on ? "Pause rollouts" : "Resume rollouts"}</button>
|
| 111 |
-
<button class="btn" type="button" data-go="settings">${icon("message", 14)}Banner</button><button class="btn" type="button" data-go="indexes">${icon("archive", 14)}Indexing & API checks</button></div></div>
|
| 112 |
-
<div class="ad-stats">
|
| 113 |
-
${stat("play", fmt.format(r.live), "running now", `${fmt.format(r.today)} in the last 24 hours`, r.live ? "live" : "")}
|
| 114 |
-
${stat("list", fmt.format(r.total), "rollouts in all", `${fmt.format(r.week)} in the last 7 days`)}
|
| 115 |
-
${stat("users", fmt.format(r.public), "on Community", `${fmt.format(r.private)} private · ${fmt.format(r.users)} people`)}
|
| 116 |
-
${stat("alert", r.finished ? `${Math.round((100 * r.failed) / r.finished)}%` : "–", "failed or stopped", `of ${fmt.format(r.finished)} finished`, r.finished && r.failed / r.finished > 0.3 ? "warn" : "")}
|
| 117 |
-
${stat("database", fmt.format(e.public.datasets), "visible datasets", `${fmt.format(e.public.tasks)} indexed task records`)}
|
| 118 |
-
${stat("globe", fmt.format(e.public.spaces), "visible checked Spaces", `${fmt.format(e.spaces)} discovery candidates`)}
|
| 119 |
-
</div>
|
| 120 |
-
${operations(o.operations)}
|
| 121 |
-
${live.length ? `<section class="panel ad-live"><div class="panel-h"><h2><i class="pulse" style="color:var(--live)"></i>Running now</h2><a class="aside" href="#" data-go="rollouts">All rollouts</a></div>
|
| 122 |
-
<div class="panel-b ad-runs">${live.map((x) => runRow(x, true)).join("")}</div></section>` : ""}
|
| 123 |
-
<div class="ad-runner-summary">${r.by_runner.map(([name,n]) => `<span class="chip">${esc(runnerName(name))} · ${fmt.format(n)}</span>`).join("")}</div>
|
| 124 |
-
<div class="ad-grid">
|
| 125 |
-
<section class="panel"><div class="panel-h"><h2>${icon("play", 15)}Rollouts, last 14 days</h2><span class="aside">${fmt.format(r.week)} in the last 7 days</span></div>
|
| 126 |
-
<div class="panel-b"><div class="ad-bars">${r.by_day.map(([d, n]) => `<div title="${esc(d)}: ${n} rollout${n === 1 ? "" : "s"}"><i style="height:${Math.max(2, (100 * n) / peak)}%"></i><span>${d.slice(8)}</span></div>`).join("")}</div>
|
| 127 |
-
${r.by_model.length ? `<div class="sec-label" style="margin-top:16px">Most-used models</div><ul class="ad-models">${r.by_model.map(([m, n]) => `<li><code>${esc(m)}</code>
|
| 128 |
-
<span class="share"><i style="width:${(100 * n) / r.by_model[0][1]}%"></i></span><b>${fmt.format(n)}</b></li>`).join("")}</ul>` : ""}</div></section>
|
| 129 |
-
<section class="panel"><div class="panel-h"><h2>${icon("flame", 15)}New on the Hub</h2><a class="aside" href="#" data-go="environments">Curate</a></div>
|
| 130 |
-
<div class="panel-b">${o.new.length ? `<ul class="ad-new">${o.new.slice(0, 9).map((x) => `<li>
|
| 131 |
-
${x.kind === "space" ? `<img class="ad-th" src="${thumb(x.id)}" alt="" loading="lazy">` : `<span class="ad-th ic-only">${icon("database", 14)}</span>`}
|
| 132 |
-
<a href="${envHref(x)}" title="${esc(x.id)}" target="_blank" rel="noopener"><code>${esc(x.id)}</code><em>${x.kind === "space" ? "Space" : "Dataset"} · ${ago(Date.parse(x.created) / 1000)}${x.collection ? ` · in ${esc(x.collection)}` : ""}</em></a></li>`).join("")}</ul>`
|
| 133 |
-
: `<p class="muted sm">Nothing new in the last two weeks.</p>`}</div></section>
|
| 134 |
-
</div>
|
| 135 |
-
<section class="panel" style="margin-top:16px"><div class="panel-h"><h2>${icon("clock", 15)}Latest rollouts</h2><a class="aside" href="#" data-go="rollouts">All rollouts</a></div>
|
| 136 |
-
<div class="panel-b ad-runs">${o.recent.length ? o.recent.map((x) => runRow(x)).join("") : emptyState("play", "No rollouts yet", "They show up here as soon as someone runs one.")}</div></section>`;
|
| 137 |
-
body.querySelectorAll("[data-go]").forEach((b) => b.addEventListener("click", (ev) => { ev.preventDefault(); go(b.dataset.go); }));
|
| 138 |
-
$("#ov-toggle", body).addEventListener("click", async () => {
|
| 139 |
-
if (on && !confirm("Pause rollouts for everyone? Running ones finish; new ones are refused until you resume.")) return;
|
| 140 |
-
try { await api("/api/admin/settings", { method: "PUT", body: { ...pick(o.settings), rollouts_enabled: !on } }); toast(on ? "Rollouts paused" : "Rollouts resumed"); overview(body, { go, s }); }
|
| 141 |
-
catch (err) { toast(err.message); }
|
| 142 |
-
});
|
| 143 |
-
runActions(body, () => overview(body, { go, s }));
|
| 144 |
-
}
|
| 145 |
-
const pick = (st) => ({ rollouts_enabled: st.rollouts_enabled, max_active: st.max_active, max_per_user: st.max_per_user, agents: st.agents, announcement: st.announcement });
|
| 146 |
-
|
| 147 |
-
// ── rollouts ─────────────────────────────────────────────────────────────────
|
| 148 |
-
async function rollouts(body, { preset }) {
|
| 149 |
-
const st = { status: "", q: "", user: preset.user || "", runner: "", visibility: "", limit: 100 };
|
| 150 |
-
let sequence = 0, pending = null;
|
| 151 |
-
body.innerHTML = `${head("Rollouts", "All saved runners, including private runs. Community eligibility requires a completed, graded, unrestricted public rollout.")}
|
| 152 |
-
<div class="ad-tools"><div class="search">${icon("search", 16)}<input id="ad-q" type="search" aria-label="Search rollouts" placeholder="Task, environment, model, person or id"></div>
|
| 153 |
-
<select class="input" id="ad-runner" aria-label="Runner"><option value="">All runners</option></select>
|
| 154 |
-
<select class="input" id="ad-vis" aria-label="Visibility"><option value="">All visibility</option><option value="community">On Community</option><option value="private">Private</option><option value="public">Public records</option><option value="moderated">Moderated</option></select>
|
| 155 |
-
<select class="input" id="ad-st" aria-label="Status">${[["", "All statuses"], ["live", "Running"], ["done", "Done"], ["failed", "Failed"], ["cancelled", "Stopped"], ["interrupted", "Interrupted"]].map(([k,l])=>`<option value="${k}">${l}</option>`).join("")}</select>
|
| 156 |
-
<button class="btn" id="ad-refresh" type="button">${icon("refresh",14)}Refresh</button><span id="ad-who"></span></div>
|
| 157 |
-
<div id="ad-run-error" role="status"></div><div class="panel"><div class="panel-b ad-runs" id="ad-runs">${sk.lines(80, 70, 90)}</div></div>
|
| 158 |
-
<div class="more-row"><button class="btn" id="ad-run-more" hidden>Load more</button></div>`;
|
| 159 |
-
const load = async () => {
|
| 160 |
-
clearTimeout(timer); pending?.abort(); pending = new AbortController(); const request = ++sequence;
|
| 161 |
-
try {
|
| 162 |
-
const d = await api(`/api/admin/rollouts?${new URLSearchParams(st)}`, {signal:pending.signal});
|
| 163 |
-
if (!alive || !body.isConnected || request !== sequence) return;
|
| 164 |
-
$("#ad-run-error",body).textContent = "";
|
| 165 |
-
$("#ad-runner",body).innerHTML = '<option value="">All runners</option>' + d.runners.map(v=>`<option value="${esc(v)}" ${v===st.runner?'selected':''}>${esc(runnerName(v))}</option>`).join("");
|
| 166 |
-
$("#ad-who", body).innerHTML = st.user ? `<button class="pill" id="ad-clear-user" type="button"><span>by</span>${esc(st.user)}${icon("x", 13)}</button>` : "";
|
| 167 |
-
$("#ad-runs", body).innerHTML = d.runs.length ? `<p class="muted sm">Showing ${fmt.format(d.runs.length)} of ${fmt.format(d.total)} rollouts</p>${d.runs.map((r) => runRow(r, true)).join("")}` : emptyState("filter", "No rollouts here", "Try another filter.");
|
| 168 |
-
$("#ad-run-more",body).hidden = d.runs.length >= d.total || st.limit >= 500;
|
| 169 |
-
if (d.total > 500 && st.limit >= 500) $("#ad-run-error",body).textContent = "Showing the latest 500 matches. Refine your filters to find older runs.";
|
| 170 |
-
if (d.runs.some(r=>r.live)) timer = setTimeout(()=>body.isConnected && load(), 5000);
|
| 171 |
-
} catch(e) { if(e.name!=="AbortError" && body.isConnected) $("#ad-run-error",body).textContent=e.message; }
|
| 172 |
-
};
|
| 173 |
-
let t;
|
| 174 |
-
$("#ad-q", body).addEventListener("input", e=>{clearTimeout(t); pending?.abort(); ++sequence; st.q=e.target.value.trim(); st.limit=100; t=setTimeout(()=>body.isConnected && load(),250);});
|
| 175 |
-
for(const [id,key] of [["ad-runner","runner"],["ad-vis","visibility"],["ad-st","status"]]) $("#"+id,body).addEventListener("change",e=>{st[key]=e.target.value;st.limit=100;load();});
|
| 176 |
-
$("#ad-refresh",body).addEventListener("click",load);
|
| 177 |
-
$("#ad-run-more",body).addEventListener("click",()=>{st.limit+=100;load();});
|
| 178 |
-
body.addEventListener("click",e=>{if(e.target.closest("#ad-clear-user")){st.user="";preset.user="";load();}});
|
| 179 |
-
runActions(body,load); await load();
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
// ── people ───────────────────────────────────────────────────────────────────
|
| 183 |
-
async function people(body, { go }) {
|
| 184 |
-
const d = await api("/api/admin/people");
|
| 185 |
-
if (!alive || !body.isConnected) return;
|
| 186 |
-
body.innerHTML = `${head("People", "Everyone who has run a rollout, most recent first.")}
|
| 187 |
-
${d.people.length ? `<div class="ad-people">${d.people.map((p) => `<article class="panel ad-person">
|
| 188 |
-
<header>${pfp(p.name, p.avatar, 40)}<div><a href="https://huggingface.co/${encodeURIComponent(p.name)}" target="_blank" rel="noopener"><b>${esc(p.name)}</b> ${icon("external", 12)}</a>
|
| 189 |
-
<em>active ${ago(p.last)}${p.live ? ` · <span class="live-txt">${p.live} running</span>` : ""}</em></div></header>
|
| 190 |
-
<div class="ad-pstats"><div><b>${fmt.format(p.runs)}</b><span>rollouts</span></div><div><b>${fmt.format(p.scored)}</b><span>graded</span></div>
|
| 191 |
-
<div><b>${p.runs ? `${Math.round((100 * p.done) / p.runs)}%` : "–"}</b><span>finished</span></div><div><b>${fmt.format(p.public)}</b><span>on Community</span></div></div>
|
| 192 |
-
<dl class="kv sm"><dt>Models</dt><dd>${p.models.map(([m, n]) => `<code>${esc(m.split("/")[1] || m)}</code> <span class="faint">${n}</span>`).join(", ")}</dd>
|
| 193 |
-
<dt>Datasets</dt><dd>${p.datasets.map(([x, n]) => `${esc(x)} <span class="faint">${n}</span>`).join(", ")}</dd>
|
| 194 |
-
<dt>Since</dt><dd>${new Date(p.first * 1000).toLocaleDateString()}</dd></dl>
|
| 195 |
-
<button class="btn sm" type="button" data-user="${esc(p.name)}">${icon("play", 12)}Their rollouts</button></article>`).join("")}</div>`
|
| 196 |
-
: `<div class="panel">${emptyState("users", "Nobody yet", "People show up here after their first rollout.")}</div>`}`;
|
| 197 |
-
}
|
| 198 |
-
|
| 199 |
-
// ── environments: what trends, what's hidden, what's featured ────────────────
|
| 200 |
-
async function environments(body) {
|
| 201 |
-
const d = await api("/api/admin/environments");
|
| 202 |
-
if (!alive || !body.isConnected) return;
|
| 203 |
-
const st = { q: "", kind: "", flag: "", health: "", shown: 60 };
|
| 204 |
-
const cols = d.collections;
|
| 205 |
-
const now = Date.now();
|
| 206 |
-
const isNew = (x) => x.created && now - Date.parse(x.created) < 14 * 864e5;
|
| 207 |
-
const n = (f) => d.environments.filter(f).length;
|
| 208 |
-
body.innerHTML = `${head("Environments", "Pin to feature separately above the trending row on Explore; hide to take off every listing (the page still opens by link); put in a collection to feature it.")}
|
| 209 |
-
<div class="ad-tools"><div class="search">${icon("search", 16)}<input id="ad-q" type="search" aria-label="Search environments" placeholder="Search environments…"></div>
|
| 210 |
-
<div class="seg" id="ad-flag">${[["", "All", d.environments.length], ["new", "New", n(isNew)], ["pinned", "Pinned", n((x) => x.pinned)], ["hidden", "Hidden", n((x) => x.hidden)],
|
| 211 |
-
["loose", "In no collection", n((x) => !x.collection)], ["unindexed", "No file index", n((x) => x.kind === "dataset" && !x.indexed)]]
|
| 212 |
-
.map(([k, l, c]) => `<button type="button" data-flag="${k}" aria-pressed="${k === ""}">${l}<span>${fmt.format(c)}</span></button>`).join("")}</div>
|
| 213 |
-
<select class="hf-select-mimo" id="ad-kind" aria-label="Environment type"><option value="">Datasets and Spaces</option><option value="dataset">Datasets</option><option value="space">Spaces</option></select><select class="input" id="ad-health" aria-label="API evidence"><option value="">All API evidence</option><option value="ready">Checked Spaces</option><option value="candidate">Unverified / unavailable Spaces</option><option value="Check expired">Expired checks</option><option value="Checks failed">OpenEnv checks failed</option><option value="Not running">Not running</option></select></div><p class="fine">Discovery candidates are kept here for review. Only Spaces with fresh supported API evidence appear in the default explorer. Versions are declared dependencies, not runtime attestations.</p>
|
| 214 |
-
<div class="panel"><div class="ad-envs" id="ad-envs"></div></div>
|
| 215 |
-
<div class="more-row"><button class="btn" id="ad-more" type="button">Show more</button></div>`;
|
| 216 |
-
const draw = () => {
|
| 217 |
-
const q = st.q.toLowerCase();
|
| 218 |
-
const rows = d.environments.filter((x) => (!st.kind || x.kind === st.kind) && (!q || `${x.id} ${x.heading || ""} ${x.tags.join(" ")}`.toLowerCase().includes(q))
|
| 219 |
-
&& (!st.health || (x.kind === "space" && (st.health === "ready" ? x.ready : st.health === "candidate" ? !x.ready : x.api_status === st.health)))
|
| 220 |
-
&& (!st.flag || (st.flag === "new" ? isNew(x) : st.flag === "pinned" ? x.pinned : st.flag === "hidden" ? x.hidden
|
| 221 |
-
: st.flag === "loose" ? !x.collection : x.kind === "dataset" && !x.indexed)))
|
| 222 |
-
.sort((a, b) => (b.pinned - a.pinned) || (b.trending - a.trending) || (b.likes - a.likes) || (b.downloads - a.downloads));
|
| 223 |
-
$("#ad-envs", body).innerHTML = rows.length ? `<div class="ad-env head"><span>Environment</span><span>Collection</span><span class="num">Trending</span><span class="num">Downloads</span>
|
| 224 |
-
<span class="num">Tasks</span><span class="num">Rollouts</span><span></span></div>` + rows.slice(0, st.shown).map((x) => `<div class="ad-env${x.hidden ? " gone" : ""}">
|
| 225 |
-
<span class="ad-name">${x.kind === "space" ? `<img class="ad-th" src="${thumb(x.id)}" alt="" loading="lazy">` : `<span class="ad-th ic-only">${icon("database", 14)}</span>`}
|
| 226 |
-
<a href="${envHref(x)}" title="${esc(x.id)}" target="_blank" rel="noopener"><code>${esc(x.id)}</code></a>
|
| 227 |
-
${x.kind === "space" ? `<span class="chip ${x.ready ? "new" : ""}" title="${esc(x.api_status || "Not checked")}">${esc(x.ready ? (x.openenv ? "OpenEnv API checked" : x.framework + " API checked") : x.api_status || "Not checked")}</span><span class="faint xs" title="${esc((x.declared_version || "Unknown version") + " · " + (x.version_source || "Unknown source"))}">${esc(x.declared_version || "Unknown version")}</span>` : `<span class="chip">${esc(x.id === "XiaomiMiMo/MiMo-V2.6-RL-oss" ? "MiMo" : x.framework === "rows" ? "RL dataset" : runnerName(x.framework))}</span>`}
|
| 228 |
-
${x.pinned ? `<span class="chip">${icon("flag", 11)}Pinned</span>` : ""}${x.hidden ? `<span class="chip">Hidden</span>` : ""}${isNew(x) ? `<span class="chip new">New</span>` : ""}</span>
|
| 229 |
-
<span><select class="hf-select-mimo sm" data-feature="${esc(x.key)}" aria-label="Collection"><option value="">No collection</option>${cols.map((c) => `<option value="${c.id}" ${x.collection === c.id ? "selected" : ""}>${esc(c.group)}</option>`).join("")}</select></span>
|
| 230 |
-
<span class="num">${fmt.format(x.trending || 0)}</span><span class="num">${x.kind === "dataset" ? compact.format(x.downloads) : `${compact.format(x.likes)} ♥`}</span>
|
| 231 |
-
<span class="num">${x.indexed?.tasks != null ? fmt.format(x.indexed.tasks) : x.kind === "dataset" && x.framework === "harbor" ? `<button class="link" data-index="${esc(x.key)}">Index</button>` : "–"}</span>
|
| 232 |
-
<span class="num">${fmt.format(x.rollouts || 0)}</span>
|
| 233 |
-
<span class="ad-act"><button class="btn sm ${x.pinned ? "" : "ghost"}" data-act="${x.pinned ? "unpin" : "pin"}" data-key="${esc(x.key)}" title="${x.pinned ? "Unpin" : "Feature above trending"}">${icon("flag", 12)}${x.pinned ? "Unpin" : "Pin"}</button>
|
| 234 |
-
<button class="btn sm ${x.hidden ? "" : "ghost"}" data-act="${x.hidden ? "unhide" : "hide"}" data-key="${esc(x.key)}">${x.hidden ? "Unhide" : "Hide"}</button></span></div>`).join("")
|
| 235 |
-
: emptyState("search", "Nothing here", "Try another filter.");
|
| 236 |
-
$("#ad-more", body).hidden = rows.length <= st.shown;
|
| 237 |
-
};
|
| 238 |
-
const act = async (key, action, extra = {}) => {
|
| 239 |
-
if (action === "hide" && !confirm(`Hide ${key.replace(/^space:/, "")} from every listing?`)) return draw();
|
| 240 |
-
try {
|
| 241 |
-
await api("/api/admin/environments", { method: "POST", body: { key, action, ...extra } });
|
| 242 |
-
const x = d.environments.find((y) => y.key === key);
|
| 243 |
-
if (x) {
|
| 244 |
-
if (action === "pin" || action === "unpin") x.pinned = action === "pin";
|
| 245 |
-
if (action === "hide" || action === "unhide") x.hidden = action === "hide";
|
| 246 |
-
if (action === "feature" || action === "unfeature") x.collection = extra.collection || null;
|
| 247 |
-
}
|
| 248 |
-
toast({ pin: "Pinned: shown as an editorial pick", unpin: "Unpinned", hide: "Hidden from listings", unhide: "Back in listings", feature: "Added to the collection",
|
| 249 |
-
unfeature: "Taken out of its collection", index: "Indexing started" }[action]);
|
| 250 |
-
draw();
|
| 251 |
-
} catch (e) { toast(e.message); }
|
| 252 |
-
};
|
| 253 |
-
let t;
|
| 254 |
-
$("#ad-q", body).addEventListener("input", (e) => { clearTimeout(t); t = setTimeout(() => { st.q = e.target.value.trim(); st.shown = 60; draw(); }, 150); });
|
| 255 |
-
$("#ad-health",body).addEventListener("change",e=>{st.health=e.target.value;st.shown=60;draw();});
|
| 256 |
-
$("#ad-kind", body).addEventListener("change", (e) => { st.kind = e.target.value; st.shown=60; draw(); });
|
| 257 |
-
$("#ad-flag", body).addEventListener("click", (e) => { const b = e.target.closest("[data-flag]"); if (!b) return; st.flag = b.dataset.flag;
|
| 258 |
-
body.querySelectorAll("#ad-flag [data-flag]").forEach((x) => x.setAttribute("aria-pressed", String(x === b))); draw(); });
|
| 259 |
-
$("#ad-more", body).addEventListener("click", () => { st.shown += 100; draw(); });
|
| 260 |
-
body.addEventListener("click", (e) => {
|
| 261 |
-
const b = e.target.closest("[data-act]");
|
| 262 |
-
if (b) act(b.dataset.key, b.dataset.act);
|
| 263 |
-
const i = e.target.closest("[data-index]");
|
| 264 |
-
if (i) { i.outerHTML = spinner(); act(i.dataset.index, "index"); }
|
| 265 |
-
});
|
| 266 |
-
body.addEventListener("change", (e) => {
|
| 267 |
-
const s = e.target.closest("[data-feature]");
|
| 268 |
-
if (s) act(s.dataset.feature, s.value ? "feature" : "unfeature", s.value ? { collection: s.value } : {});
|
| 269 |
-
});
|
| 270 |
-
draw();
|
| 271 |
-
}
|
| 272 |
-
|
| 273 |
-
// ── collections ──────────────────────────────────────────────────────────────
|
| 274 |
-
async function collections(body) {
|
| 275 |
-
const d = await api("/api/admin/collections");
|
| 276 |
-
if (!alive || !body.isConnected) return;
|
| 277 |
-
let cols = d.collections.map((c) => ({ ...c, ids: [...c.ids] }));
|
| 278 |
-
let dirty = false;
|
| 279 |
-
const draw = () => {
|
| 280 |
-
body.innerHTML = `${head("Collections", "The tiles on Explore, in this order. One environment per line: a dataset id, or <code>space:org/name</code> for a Space.",
|
| 281 |
-
`<button class="btn" id="ad-add" type="button">${icon("plus", 14)}Add a collection</button><button class="btn primary" id="ad-save" type="button" ${dirty ? "" : "disabled"}>${icon("check", 14)}Save</button>`)}
|
| 282 |
-
<div class="ad-cols">${cols.map((c, i) => `<section class="panel" data-i="${i}"><div class="panel-h"><h2><span class="ad-swatch" style="--dc:var(--c-${esc(c.color)})">${icon(c.icon, 14)}</span>${esc(c.group || "New collection")}
|
| 283 |
-
<span class="faint xs">${c.ids.length} environment${c.ids.length === 1 ? "" : "s"}</span></h2>
|
| 284 |
-
<span class="aside"><button class="icon-btn" data-up="${i}" ${i ? "" : "disabled"} aria-label="Move up" title="Move up">${icon("chevronDown", 14, "up")}</button>
|
| 285 |
-
<button class="icon-btn" data-down="${i}" ${i < cols.length - 1 ? "" : "disabled"} aria-label="Move down" title="Move down">${icon("chevronDown", 14)}</button>
|
| 286 |
-
<button class="icon-btn" data-del="${i}" aria-label="Delete" title="Delete">${icon("x", 14)}</button></span></div>
|
| 287 |
-
<div class="panel-b ad-form">
|
| 288 |
-
<label class="field"><span>Name</span><input class="input" data-f="group" value="${esc(c.group)}" maxlength="40"></label>
|
| 289 |
-
<label class="field"><span>Id <em>lowercase, for links</em></span><input class="input mono" data-f="id" value="${esc(c.id)}" maxlength="31"></label>
|
| 290 |
-
<label class="field wide"><span>About</span><input class="input" data-f="about" value="${esc(c.about || "")}" maxlength="80"></label>
|
| 291 |
-
<label class="field"><span>Icon</span><select class="hf-select-mimo" data-f="icon">${d.icons.map((x) => `<option ${x === c.icon ? "selected" : ""}>${x}</option>`).join("")}</select></label>
|
| 292 |
-
<label class="field"><span>Colour</span><select class="hf-select-mimo" data-f="color">${d.colors.map((x) => `<option ${x === c.color ? "selected" : ""}>${x}</option>`).join("")}</select></label>
|
| 293 |
-
<label class="field wide"><span>Environments</span><textarea class="input mono" data-f="ids" rows="${Math.min(10, c.ids.length + 2)}">${esc(c.ids.join("\n"))}</textarea></label>
|
| 294 |
-
</div></section>`).join("")}</div>`;
|
| 295 |
-
};
|
| 296 |
-
const read = () => body.querySelectorAll("[data-i]").forEach((sec) => {
|
| 297 |
-
const c = cols[+sec.dataset.i];
|
| 298 |
-
sec.querySelectorAll("[data-f]").forEach((f) => { c[f.dataset.f] = f.dataset.f === "ids" ? f.value.split("\n").map((x) => x.trim()).filter(Boolean) : f.value.trim(); });
|
| 299 |
-
});
|
| 300 |
-
const touch = () => { dirty = true; const b = $("#ad-save", body); if (b) b.disabled = false; };
|
| 301 |
-
body.addEventListener("input", touch);
|
| 302 |
-
body.addEventListener("change", touch);
|
| 303 |
-
body.addEventListener("click", async (e) => {
|
| 304 |
-
const up = e.target.closest("[data-up]"), down = e.target.closest("[data-down]"), del = e.target.closest("[data-del]");
|
| 305 |
-
if (up || down || del) { read(); dirty = true; }
|
| 306 |
-
if (up) { const i = +up.dataset.up; [cols[i - 1], cols[i]] = [cols[i], cols[i - 1]]; draw(); }
|
| 307 |
-
else if (down) { const i = +down.dataset.down; [cols[i + 1], cols[i]] = [cols[i], cols[i + 1]]; draw(); }
|
| 308 |
-
else if (del) { if (confirm(`Delete the collection “${cols[+del.dataset.del].group}”? Its environments stay on the Hub.`)) { cols.splice(+del.dataset.del, 1); draw(); } }
|
| 309 |
-
else if (e.target.closest("#ad-add")) { read(); dirty = true; cols.push({ id: `new-${cols.length + 1}`, group: "New collection", about: "", icon: "grid", color: "code", ids: [] }); draw(); }
|
| 310 |
-
else if (e.target.closest("#ad-save")) {
|
| 311 |
-
read();
|
| 312 |
-
try { cols = (await api("/api/admin/collections", { method: "PUT", body: { collections: cols } })).collections.map((c) => ({ ...c, ids: [...c.ids] })); dirty = false; draw(); toast("Saved: Explore shows them within seconds"); }
|
| 313 |
-
catch (err) { toast(err.message); }
|
| 314 |
-
}
|
| 315 |
-
});
|
| 316 |
-
draw();
|
| 317 |
-
}
|
| 318 |
-
|
| 319 |
-
function operations(o) {
|
| 320 |
-
const job=o.indexer, snap=o.snapshot, c=o.checks, tasks=o.task_catalog;
|
| 321 |
-
const state=job.stale ? "Update overdue" : job.state === "indexing" ? "Indexing" : job.state === "retrying" ? "Retry scheduled" : job.state ? "Waiting for next cycle" : "Waiting for first report";
|
| 322 |
-
const when=v=>v ? ago(v) : "Not recorded";
|
| 323 |
-
return `<section class="panel ad-operations"><div class="panel-h"><h2>${icon("refresh",15)}Catalog operations</h2><span class="aside">Shared explorer state</span></div><div class="panel-b ad-ops-grid">
|
| 324 |
-
<div><b>Automatic indexing</b><p class="${job.stale || job.error ? 'err-text' : 'muted'}">${esc(state)}</p><dl class="kv sm"><dt>Last success</dt><dd>${when(job.last_success)}</dd><dt>Next attempt</dt><dd>${job.next_at ? esc(new Date(job.next_at*1000).toLocaleString()) : "Not recorded"}</dd><dt>Snapshot</dt><dd>${snap.built_at ? esc(new Date(snap.built_at).toLocaleString()) : "No snapshot yet"}</dd></dl>${job.error || snap.error ? `<p class="err-text">${esc(job.error || snap.error)}</p>` : ''}</div>
|
| 325 |
-
<div><b>Space API checks</b><p>${fmt.format(c.ready)} checked · ${fmt.format(c.openenv)} OpenEnv · ${fmt.format(c.candidates)} candidates</p><dl class="kv sm"><dt>Latest check</dt><dd>${when(c.latest_at)}</dd><dt>Freshness</dt><dd>${c.fresh_seconds/60} minutes</dd><dt>Reported tasks</dt><dd>${fmt.format(tasks.tasks)} in ${fmt.format(tasks.spaces)} Spaces${tasks.partial_spaces ? ` · ${tasks.partial_spaces} partial catalogs` : ''}</dd></dl></div>
|
| 326 |
-
<div class="ad-check-states">${Object.entries(c.statuses).map(([v,n])=>`<span class="chip">${esc(v)} · ${fmt.format(n)}</span>`).join('')}</div>
|
| 327 |
-
<p class="fine ad-ops-note">Task entries reported by Space APIs are separate from downloaded, searchable task records. Checks do not wake sleeping Spaces or certify grading quality. The explorer runs maintenance automatically while it is online.</p>
|
| 328 |
-
${c.versions.length ? `<details class="ad-ops-note"><summary>Declared OpenEnv versions (${c.versions.length})</summary><div class="ad-check-states">${c.versions.map(([v,n])=>`<span class="chip">${esc(v)} · ${fmt.format(n)}</span>`).join('')}</div></details>` : ''}
|
| 329 |
-
</div></section>`;
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
// ── indexes ──────────────────────────────────────────────────────────────────
|
| 333 |
-
async function indexes(body) {
|
| 334 |
-
const d = await api("/api/admin/indexes");
|
| 335 |
-
if (!alive || !body.isConnected) return;
|
| 336 |
-
body.indexEvents?.abort(); body.indexEvents = new AbortController();
|
| 337 |
-
const jobs = Object.entries(d.jobs);
|
| 338 |
-
body.innerHTML = `${head("Indexing & API checks", "Automatic maintenance, fresh protocol evidence and dataset indexes across frameworks.", '<button class="btn" id="ad-index-refresh">Refresh status</button>')}
|
| 339 |
-
${operations(d.operations)}
|
| 340 |
-
<div class="ad-tools"><div class="search">${icon("database", 16)}<input id="ad-spec" placeholder="A public Harbor dataset: org/name"></div>
|
| 341 |
-
<button class="btn primary" id="ad-go" type="button">${icon("play", 14)}Index it</button></div>
|
| 342 |
-
${jobs.length ? `<section class="panel" style="margin-bottom:12px"><div class="panel-h"><h2>${spinner()}Indexing now</h2></div><div class="panel-b"><dl class="kv">${jobs.map(([k, j]) =>
|
| 343 |
-
`<dt><code>${esc(k)}</code></dt><dd>${esc(j.state)}${j.total ? ` · ${fmt.format(j.done || 0)} of ${fmt.format(j.total)}` : ""}${j.error ? ` · <span class="err-text">${esc(j.error)}</span>` : ""}</dd>`).join("")}</dl></div></section>` : ""}
|
| 344 |
-
<div class="panel"><div class="tbl" style="max-height:none;border:0"><table><thead><tr><th>Dataset</th><th class="num">Tasks</th><th>Built</th><th class="num">Index</th><th class="num">Pack</th><th></th></tr></thead><tbody>
|
| 345 |
-
${d.indexes.map((x) => `<tr><td><a class="u" href="${EXPLORER}/d/${enc(x.spec)}" target="_blank" rel="noopener"><code>${esc(x.spec)}</code></a></td><td class="num">${x.tasks != null ? fmt.format(x.tasks) : "–"}</td>
|
| 346 |
-
<td>${x.built ? ago(x.built) : "–"}${x.current ? "" : ` <span class="chip">old format</span>`}</td><td class="num">${bytes(x.bytes)}</td><td class="num">${bytes(x.pack_bytes)}</td>
|
| 347 |
-
<td><button class="btn sm ghost" data-reindex="${esc(x.spec)}">${icon("refresh", 12)}Rebuild</button></td></tr>`).join("")}</tbody></table></div></div>`;
|
| 348 |
-
const go = async (spec) => {
|
| 349 |
-
try { const result = await api("/api/admin/environments", { method: "POST", body: { key: spec, action: "index" } }); toast(result.job?.state === "done" ? "Index is current" : result.job?.state === "error" ? result.job.error || "Indexing failed" : `Indexing ${spec}`); timer = setTimeout(() => alive && body.isConnected && indexes(body), 1500); }
|
| 350 |
-
catch (e) { toast(e.message); }
|
| 351 |
-
};
|
| 352 |
-
$("#ad-index-refresh",body).addEventListener("click",()=>indexes(body));
|
| 353 |
-
$("#ad-go", body).addEventListener("click", () => { const v = $("#ad-spec", body).value.trim(); if (v) go(v); });
|
| 354 |
-
$("#ad-spec", body).addEventListener("keydown", (e) => { if (e.key === "Enter") $("#ad-go", body).click(); });
|
| 355 |
-
body.addEventListener("click", (e) => { const b = e.target.closest("[data-reindex]"); if (b) go(b.dataset.reindex); }, {signal:body.indexEvents.signal});
|
| 356 |
-
if (jobs.length) timer = setTimeout(() => alive && body.isConnected && indexes(body), 3000);
|
| 357 |
-
}
|
| 358 |
-
|
| 359 |
-
// ── settings ─────────────────────────────────────────────────────────────────
|
| 360 |
-
async function settings(body) {
|
| 361 |
-
const s = await api("/api/admin/settings");
|
| 362 |
-
if (!alive || !body.isConnected) return;
|
| 363 |
-
body.innerHTML = `${head("Settings", "They apply to the explorer within seconds.")}
|
| 364 |
-
<section class="panel"><div class="panel-b ad-settings">
|
| 365 |
-
<div class="ad-set"><div><b>Rollouts</b><span>Off pauses new rollouts for everyone; running ones finish.</span></div>
|
| 366 |
-
<label class="toggle"><input type="checkbox" id="st-on" aria-label="Enable rollouts" ${s.rollouts_enabled ? "checked" : ""}><i></i></label></div>
|
| 367 |
-
<div class="ad-set"><div><b>Rollouts at once</b><span>The whole explorer, then each person.</span></div>
|
| 368 |
-
<div class="ad-nums"><input class="input" type="number" id="st-max" min="1" max="200" value="${s.max_active}" aria-label="The whole explorer"><span class="faint">/</span>
|
| 369 |
-
<input class="input" type="number" id="st-user" min="1" max="20" value="${s.max_per_user}" aria-label="Each person"></div></div>
|
| 370 |
-
<div class="ad-set"><div><b>Harbor agent harnesses</b><span>Controls Harbor runs. MiMo and NeMo Gym use their own supported execution paths.</span></div>
|
| 371 |
-
<div class="ad-checks">${s.all_agents.map((a) => `<label><input type="checkbox" data-agent="${a.id}" ${s.agents.includes(a.id) ? "checked" : ""}> ${esc(a.name)}</label>`).join("")}</div></div>
|
| 372 |
-
<div class="ad-set col"><div><b>Announcement</b><span>One line above every page of the explorer; empty for none.</span></div>
|
| 373 |
-
<input class="input" id="st-ann" maxlength="280" value="${esc(s.announcement)}" placeholder="e.g. Rollouts are slow today: a provider is overloaded.">
|
| 374 |
-
<div class="announce ad-preview" id="st-prev" ${s.announcement ? "" : "hidden"}><div class="wrap">${icon("info", 14)}<span>${esc(s.announcement)}</span></div></div></div>
|
| 375 |
-
<div class="ad-save-row"><span class="faint sm" id="st-dirty"></span><button class="btn primary" id="st-save" type="button">${icon("check", 14)}Save settings</button></div>
|
| 376 |
-
</div></section>`;
|
| 377 |
-
const mark = () => { $("#st-dirty", body).textContent = "Unsaved changes"; };
|
| 378 |
-
body.addEventListener("input", (e) => {
|
| 379 |
-
mark();
|
| 380 |
-
if (e.target.id === "st-ann") { $("#st-prev", body).hidden = !e.target.value.trim(); $("#st-prev span", body).textContent = e.target.value; }
|
| 381 |
-
});
|
| 382 |
-
body.addEventListener("change", mark);
|
| 383 |
-
$("#st-save", body).addEventListener("click", async () => {
|
| 384 |
-
const on = $("#st-on", body).checked;
|
| 385 |
-
if (!on && s.rollouts_enabled && !confirm("Pause rollouts for everyone?")) return;
|
| 386 |
-
try {
|
| 387 |
-
const r = await api("/api/admin/settings", { method: "PUT", body: { rollouts_enabled: on, max_active: +$("#st-max", body).value,
|
| 388 |
-
max_per_user: +$("#st-user", body).value, agents: [...body.querySelectorAll("[data-agent]:checked")].map((x) => x.dataset.agent), announcement: $("#st-ann", body).value.trim() } });
|
| 389 |
-
Object.assign(s, r);
|
| 390 |
-
$("#st-dirty", body).textContent = "";
|
| 391 |
-
toast("Saved");
|
| 392 |
-
} catch (e) { toast(e.message); }
|
| 393 |
-
});
|
| 394 |
-
}
|
| 395 |
-
|
| 396 |
-
// ── audit ────────────────────────────────────────────────────────────────────
|
| 397 |
-
async function audit(body) {
|
| 398 |
-
const [d, ppl] = await Promise.all([api("/api/admin/audit"), api("/api/admin/people").catch(() => ({ people: [] }))]);
|
| 399 |
-
if (!alive || !body.isConnected) return;
|
| 400 |
-
const pics = Object.fromEntries(ppl.people.map((p) => [p.name, p.avatar]));
|
| 401 |
-
const me = getSession().user;
|
| 402 |
-
if (me) pics[me.name] = pics[me.name] || me.avatar;
|
| 403 |
-
body.innerHTML = `${head("Audit log", "Every change made here, newest first.")}<div class="panel"><div class="panel-b">${d.entries.length ? `<ol class="ad-audit">${d.entries.map((x) => `<li>
|
| 404 |
-
${person(x.user, pics[x.user])}<span>${esc(x.note || Object.keys(x.after || {}).join(", ") || "changed settings")}</span><span class="faint" title="${new Date(x.at * 1000).toLocaleString()}">${ago(x.at)}</span></li>`).join("")}</ol>`
|
| 405 |
-
: emptyState("list", "Nothing yet", "Changes made here show up in this log.")}</div></div>`;
|
| 406 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web-admin/index.html
DELETED
|
@@ -1,41 +0,0 @@
|
|
| 1 |
-
<!doctype html>
|
| 2 |
-
<html lang="en">
|
| 3 |
-
<head>
|
| 4 |
-
<meta charset="utf-8">
|
| 5 |
-
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
| 6 |
-
<title>HF RL Explorer admin</title>
|
| 7 |
-
<meta name="robots" content="noindex">
|
| 8 |
-
<link rel="icon" href="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" type="image/svg+xml">
|
| 9 |
-
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
-
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 12 |
-
<link rel="stylesheet" href="/app.css">
|
| 13 |
-
<link rel="stylesheet" href="/rlx.css">
|
| 14 |
-
<link rel="stylesheet" href="/admin/admin.css">
|
| 15 |
-
<script src="/js/boot.js"></script>
|
| 16 |
-
</head>
|
| 17 |
-
<body>
|
| 18 |
-
<div class="progress" id="progress" aria-hidden="true"><i></i></div>
|
| 19 |
-
<header class="top">
|
| 20 |
-
<div class="wrap top-inner">
|
| 21 |
-
<a class="brand" href="#/" aria-label="HF RL Explorer admin, home">
|
| 22 |
-
<img class="logo" src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="" width="30" height="30">
|
| 23 |
-
<span class="name">HF RL Explorer <span>Admin</span></span>
|
| 24 |
-
</a>
|
| 25 |
-
<nav class="nav" aria-label="Main"><a href="#/" class="on">Dashboard</a><a id="to-explorer" href="/" target="_blank" rel="noopener">Explorer ↗</a></nav>
|
| 26 |
-
<div class="top-right">
|
| 27 |
-
<button class="icon-btn" id="theme" type="button" aria-label="Toggle dark mode" title="Toggle dark mode"></button>
|
| 28 |
-
<div id="account"></div>
|
| 29 |
-
</div>
|
| 30 |
-
</div>
|
| 31 |
-
</header>
|
| 32 |
-
<main id="view"><div class="boot" aria-label="Loading"><span class="spinner lg"></span></div></main>
|
| 33 |
-
<div class="scrim" id="scrim" hidden></div>
|
| 34 |
-
<div class="modal" id="modal" role="dialog" aria-modal="true" hidden><div class="modal-bar"><span id="modal-icon"></span><span id="modal-title"></span><span class="grow"></span>
|
| 35 |
-
<a class="btn sm" id="modal-open" target="_blank" rel="noopener" hidden></a><button class="icon-btn" id="modal-close" type="button" aria-label="Close"></button></div><div class="modal-body" id="modal-body"></div></div>
|
| 36 |
-
<div id="dialog-root"></div>
|
| 37 |
-
<div class="toast" id="toast" role="status" hidden></div>
|
| 38 |
-
<footer class="foot"><div class="wrap"><span>HF RL Explorer admin · private to the FineEnvs organization</span><span class="grow"></span><span id="foot-version"></span></div></footer>
|
| 39 |
-
<script type="module" src="/admin/main.js"></script>
|
| 40 |
-
</body>
|
| 41 |
-
</html>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
web-admin/main.js
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
// The admin Space's shell: who you are, the theme, and the dashboard (web-admin/admin.js).
|
| 2 |
-
import { $, api, esc, storage } from "/js/util.js";
|
| 3 |
-
import { icon } from "/js/icons.js";
|
| 4 |
-
import { getSession, setSession } from "/js/session.js";
|
| 5 |
-
import * as dash from "/admin/admin.js";
|
| 6 |
-
|
| 7 |
-
async function boot() {
|
| 8 |
-
try { setSession(await api("/api/me")); } catch { setSession({ user: null }); }
|
| 9 |
-
const s = getSession();
|
| 10 |
-
$("#to-explorer").href = s.explorer || "/";
|
| 11 |
-
if (s.version) $("#foot-version").innerHTML = `v${esc(s.version)} · <code>${esc(s.source || "")}</code>`;
|
| 12 |
-
$("#account").innerHTML = s.user ? `<span class="acct-btn" title="${esc(s.user.name)}">${s.user.avatar ? `<img src="${esc(s.user.avatar)}" alt="">` : ""}<span class="name">${esc(s.user.name)}</span></span>
|
| 13 |
-
${s.local ? "" : `<button class="btn sm ghost" id="signout" type="button">${icon("logout", 13)}Sign out</button>`}` : "";
|
| 14 |
-
$("#signout")?.addEventListener("click", async () => { await api("/api/logout", { method: "POST" }).catch(() => {}); location.reload(); });
|
| 15 |
-
const route = () => {
|
| 16 |
-
dash.unmount();
|
| 17 |
-
const qs = new URLSearchParams((location.hash.split("?")[1]) || "");
|
| 18 |
-
dash.mount($("#view"), { qs, explorer: s.explorer });
|
| 19 |
-
};
|
| 20 |
-
addEventListener("hashchange", route);
|
| 21 |
-
route();
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
const isDark = () => (document.documentElement.dataset.theme ? document.documentElement.dataset.theme === "dark" : matchMedia("(prefers-color-scheme: dark)").matches);
|
| 25 |
-
const paint = () => { $("#theme").innerHTML = icon(isDark() ? "sun" : "moon", 17); };
|
| 26 |
-
paint();
|
| 27 |
-
$("#theme").addEventListener("click", () => { document.documentElement.dataset.theme = isDark() ? "light" : "dark"; storage.set("theme", document.documentElement.dataset.theme); paint(); });
|
| 28 |
-
boot();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|