Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
148 lines
7.3 KiB
Python
148 lines
7.3 KiB
Python
"""The rate route over real HTTP, mounted by core's module-routes hook."""
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
pytest.importorskip("aiohttp")
|
|
|
|
from core.tests.test_routes_shortcuts import _request, server # noqa: F401,E402
|
|
from modules.system.taste import store # noqa: E402
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def root(tmp_path, monkeypatch):
|
|
monkeypatch.setattr(store, "ROOT", tmp_path / "taste")
|
|
|
|
|
|
def test_rate_and_list(server): # noqa: F811
|
|
store.capture("fox", "reins", {1: torch.ones(2)}, prompt_id="p1")
|
|
status, out = _request(server, "POST", "/funpack/api/m/taste/rate",
|
|
{"prompt_id": "p1", "rating": "liked"})
|
|
assert status == 200 and out["recorded"] == ["reins"], out
|
|
status, out = _request(server, "POST", "/funpack/api/m/taste/rate",
|
|
{"prompt_id": "old", "rating": "liked"})
|
|
assert status == 200 and out["why"], out
|
|
status, out = _request(server, "POST", "/funpack/api/m/taste/rate",
|
|
{"prompt_id": "p1", "rating": "meh"})
|
|
assert status == 400
|
|
assert _request(server, "GET", "/funpack/api/m/taste/keys")[1] == {"keys": ["fox"]}
|
|
assert _request(server, "DELETE", "/funpack/api/m/taste/keys/fox")[1] == {"keys": [], "deleted": "fox", "removed": 1}
|
|
|
|
|
|
def test_a_dislike_over_http_carries_its_axis(server): # noqa: F811
|
|
store.capture("fox", "reins", {1: torch.ones(2)}, prompt_id="p1")
|
|
status, out = _request(server, "POST", "/funpack/api/m/taste/rate",
|
|
{"prompt_id": "p1", "rating": "disliked", "axis": "image"})
|
|
assert status == 200 and out["recorded"] == ["reins"]
|
|
assert store.load("fox", "reins")["rows"][0]["axis"] == "image"
|
|
status, _ = _request(server, "POST", "/funpack/api/m/taste/rate",
|
|
{"prompt_id": "p1", "rating": "liked", "axis": "image"})
|
|
assert status == 400
|
|
|
|
|
|
def _raw(server, method, path, body=b""):
|
|
import http.client
|
|
conn = http.client.HTTPConnection("127.0.0.1", server)
|
|
conn.request(method, path, body=body)
|
|
resp = conn.getresponse()
|
|
data = resp.read()
|
|
return resp.status, data
|
|
|
|
|
|
def test_a_key_exports_and_imports_back_under_another_name(server): # noqa: F811
|
|
store.capture("fox", "reins", {1: torch.ones(2)}, prompt_id="p1")
|
|
_request(server, "POST", "/funpack/api/m/taste/rate", {"prompt_id": "p1", "rating": "liked"})
|
|
status, zipped = _raw(server, "GET", "/funpack/api/m/taste/keys/fox/export")
|
|
assert status == 200 and zipped[:2] == b"PK"
|
|
status, out = _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=wolf", zipped)
|
|
assert status == 200 and b'"wolf"' in out
|
|
assert store.load("wolf", "reins")["rows"][0]["reward"] == 1.0
|
|
status, out = _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=wolf", zipped)
|
|
assert status == 409 and b'"exists": true' in out
|
|
assert _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=wolf&overwrite=1", zipped)[0] == 200
|
|
|
|
|
|
def test_an_import_refuses_what_is_not_a_key(server): # noqa: F811
|
|
import io
|
|
import zipfile
|
|
assert _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=x", b"not a zip")[0] == 400
|
|
for name in ("../evil.pt", "sub/dir.pt", "notes.txt", "reins.pending.pt"):
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w") as zf:
|
|
zf.writestr(name, b"x")
|
|
assert _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=x", buf.getvalue())[0] == 400, name
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w") as zf:
|
|
zf.writestr("reins.pt", b"this is not a torch file")
|
|
status, out = _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=x", buf.getvalue())
|
|
assert status == 400 and b"readable" in out
|
|
assert store.keys() == [] # nothing half-imported
|
|
assert _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=..%2Fx", b"")[0] == 400
|
|
|
|
|
|
def _zip_of(files):
|
|
import io
|
|
import zipfile
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w") as zf:
|
|
for name, data in files.items():
|
|
zf.writestr(name, data)
|
|
return buf.getvalue()
|
|
|
|
|
|
def _torch_bytes(obj):
|
|
import io
|
|
buf = io.BytesIO()
|
|
torch.save(obj, buf)
|
|
return buf.getvalue()
|
|
|
|
|
|
def test_an_import_with_rows_the_learners_cannot_read_is_refused(server): # noqa: F811
|
|
for bad in ({"rows": [1, 2]}, {"rows": [{"reward": "high", "rows": {}}]},
|
|
{"rows": [{"reward": 1.0, "rows": {"1": 5}}]},
|
|
{"rows": [{"reward": 1.0, "axis": "mood", "rows": {"1": torch.ones(1)}}]}):
|
|
status, out = _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=x",
|
|
_zip_of({"reins.pt": _torch_bytes(bad)}))
|
|
assert status == 400 and b"shape FunPack reads" in out, bad
|
|
good = {"rows": [{"prompt_id": "p", "reward": 1.0, "rows": {"1": torch.ones(2)}}]}
|
|
assert _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=x", _zip_of({"reins.pt": _torch_bytes(good)}))[0] == 200
|
|
|
|
|
|
def test_overwriting_a_key_drops_the_waiting_capture_so_the_next_rating_says_so(server): # noqa: F811
|
|
good = _zip_of({"reins.pt": _torch_bytes({"rows": []})})
|
|
store.capture("fox", "reins", {1: torch.ones(2)}, prompt_id="p1")
|
|
assert _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=fox&overwrite=1", good)[0] == 200
|
|
assert store.latest_key() is None
|
|
|
|
|
|
def test_a_damaged_zip_and_an_empty_key_are_sentences_not_500s(server): # noqa: F811
|
|
data = bytearray(_zip_of({"reins.pt": b"x" * 4000}))
|
|
data[40:60] = b"\xff" * 20 # inside the entry's bytes
|
|
status, out = _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=x", bytes(data))
|
|
assert status == 400, out
|
|
store.capture("fox", "reins", {1: torch.ones(2)}, prompt_id="p1") # waiting capture only
|
|
status, out = _raw(server, "GET", "/funpack/api/m/taste/keys/fox/export")
|
|
assert status == 400 and b"nothing to export" in out
|
|
assert not store.valid("a ")
|
|
|
|
|
|
def test_reserved_names_shapes_and_missing_prompt_ids_are_refused(server): # noqa: F811
|
|
good = {"rows": [{"prompt_id": "p", "reward": 1.0, "rows": {"1": torch.ones(2)}}]}
|
|
assert _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=latest.json", _zip_of({"reins.pt": _torch_bytes(good)}))[0] == 400
|
|
mixed = {"rows": [{"prompt_id": "p", "reward": 1.0, "rows": {"1": torch.ones(2)}},
|
|
{"prompt_id": "q", "reward": -1.0, "rows": {"1": torch.ones(3)}}]}
|
|
assert _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=x", _zip_of({"reins.pt": _torch_bytes(mixed)}))[0] == 400
|
|
nopid = {"rows": [{"reward": 1.0, "rows": {"1": torch.ones(2)}}]}
|
|
assert _raw(server, "POST", "/funpack/api/m/taste/keys/import?name=x", _zip_of({"reins.pt": _torch_bytes(nopid)}))[0] == 400
|
|
status, out = _request(server, "POST", "/funpack/api/m/taste/rate", {"rating": "liked"})
|
|
assert status == 400
|
|
|
|
|
|
def test_the_generation_route_forgets_unrated_runs_over_http(server): # noqa: F811
|
|
store.capture("fox", "reins", {1: torch.ones(2)}, prompt_id="p1")
|
|
store.capture("fox", "reins", {1: torch.ones(2)}, prompt_id="p2")
|
|
status, out = _request(server, "POST", "/funpack/api/m/taste/generation", {})
|
|
assert status == 200 and out == {"dropped": 2}
|
|
status, out = _request(server, "POST", "/funpack/api/m/taste/rate", {"prompt_id": "p1", "rating": "liked"})
|
|
assert status == 200 and out["recorded"] == [] and "new Generate" in out["why"]
|