Spaces:
Sleeping
Sleeping
| """Covers the read/browse half of the console — conversations, dataset | |
| snapshots and training jobs (`/v1/console/sessions|datasets|training-jobs`). | |
| The chat/voice turns that produce this data are covered in | |
| test_console_chat.py / test_console_voice.py.""" | |
| from app.database import SessionLocal | |
| from app.models import Dataset, DatasetItem, TrainingRun | |
| from app.services import model_client | |
| async def _fake_get_reply(messages, language): | |
| return f"echo:{messages[-1]['content']}", "groq" | |
| def _signup(client, email: str) -> dict: | |
| resp = client.post("/v1/auth/signup", json={ | |
| "name": "Rumi", "email": email, "company": "DOS", "password": "trainmodel123", | |
| }) | |
| assert resp.status_code == 200, resp.text | |
| return {"Authorization": f"Bearer {resp.json()['access_token']}"} | |
| def _console_user(client, monkeypatch, email: str) -> dict: | |
| """Signs up an allowlisted operator and burns one chat turn so the | |
| self-serve promotion to console_admin has actually happened.""" | |
| monkeypatch.setattr(model_client, "get_reply", _fake_get_reply) | |
| monkeypatch.setenv("CONSOLE_ADMIN_EMAILS", email) | |
| headers = _signup(client, email) | |
| return headers | |
| # --------------------------------------------------------------------------- | |
| # Auth gating — same door as chat/voice | |
| # --------------------------------------------------------------------------- | |
| def test_explorer_routes_require_login(client): | |
| for path in ("/v1/console/sessions", "/v1/console/datasets", "/v1/console/training-jobs"): | |
| resp = client.get(path) | |
| assert resp.status_code == 401, path | |
| assert resp.json()["error"]["code"] == "UNAUTHORIZED" | |
| def test_explorer_routes_forbidden_for_non_console_user(client, auth_headers, monkeypatch): | |
| monkeypatch.delenv("CONSOLE_ADMIN_EMAILS", raising=False) | |
| headers = auth_headers("+15557770001", "Driver Dana") | |
| for path in ("/v1/console/sessions", "/v1/console/datasets", "/v1/console/training-jobs"): | |
| resp = client.get(path, headers=headers) | |
| assert resp.status_code == 403, path | |
| # --------------------------------------------------------------------------- | |
| # Conversations | |
| # --------------------------------------------------------------------------- | |
| def test_sessions_summarise_turns(client, monkeypatch): | |
| headers = _console_user(client, monkeypatch, "explorer1@example.com") | |
| first = client.post( | |
| "/v1/console/chat", headers=headers, json={"text": "turn one"} | |
| ).json() | |
| client.post( | |
| "/v1/console/chat", | |
| headers=headers, | |
| json={"text": "turn two", "session_id": first["session_id"]}, | |
| ) | |
| rows = client.get("/v1/console/sessions", headers=headers).json() | |
| assert len(rows) == 1 | |
| assert rows[0]["id"] == first["session_id"] | |
| assert rows[0]["turn_count"] == 2 | |
| # Preview is the first thing said, not the most recent. | |
| assert rows[0]["preview"] == "turn one" | |
| assert rows[0]["last_activity"] >= rows[0]["started_at"] | |
| def test_session_detail_returns_turns_in_order(client, monkeypatch): | |
| headers = _console_user(client, monkeypatch, "explorer2@example.com") | |
| started = client.post( | |
| "/v1/console/chat", headers=headers, json={"text": "first"} | |
| ).json() | |
| client.post( | |
| "/v1/console/chat", | |
| headers=headers, | |
| json={"text": "second", "session_id": started["session_id"]}, | |
| ) | |
| detail = client.get( | |
| f"/v1/console/sessions/{started['session_id']}", headers=headers | |
| ).json() | |
| assert [t["input_text"] for t in detail["turns"]] == ["first", "second"] | |
| assert detail["turns"][0]["output_text"] == "echo:first" | |
| assert detail["turns"][0]["latency_ms"] is not None | |
| def test_session_detail_404s_for_unknown_id(client, monkeypatch): | |
| headers = _console_user(client, monkeypatch, "explorer3@example.com") | |
| client.post("/v1/console/chat", headers=headers, json={"text": "hi"}) | |
| resp = client.get("/v1/console/sessions/not-a-real-session", headers=headers) | |
| assert resp.status_code == 404 | |
| def test_sessions_are_shared_between_console_users(client, monkeypatch): | |
| """Console data is org-wide, not per-account: datasets and model versions | |
| are built from every operator's turns, so every operator can review them.""" | |
| monkeypatch.setattr(model_client, "get_reply", _fake_get_reply) | |
| monkeypatch.setenv("CONSOLE_ADMIN_EMAILS", "op-a@example.com,op-b@example.com") | |
| a = _signup(client, "op-a@example.com") | |
| b = _signup(client, "op-b@example.com") | |
| client.post("/v1/console/chat", headers=a, json={"text": "logged by A"}) | |
| rows = client.get("/v1/console/sessions", headers=b).json() | |
| assert len(rows) == 1 | |
| assert rows[0]["preview"] == "logged by A" | |
| # --------------------------------------------------------------------------- | |
| # Datasets | |
| # --------------------------------------------------------------------------- | |
| def test_build_dataset_rejects_empty_corpus(client, monkeypatch): | |
| headers = _console_user(client, monkeypatch, "explorer4@example.com") | |
| resp = client.post("/v1/console/datasets", headers=headers) | |
| assert resp.status_code == 400 | |
| assert client.get("/v1/console/datasets", headers=headers).json() == [] | |
| def test_build_dataset_freezes_turns_into_items(client, monkeypatch): | |
| """The snapshot must copy the turns into dataset_item rows, not just | |
| record a count — a dataset that re-queries interactions at train time | |
| isn't frozen and can't be reproduced (C-12).""" | |
| headers = _console_user(client, monkeypatch, "explorer5@example.com") | |
| client.post("/v1/console/chat", headers=headers, json={"text": "keep me"}) | |
| client.post("/v1/console/chat", headers=headers, json={"text": "and me"}) | |
| resp = client.post("/v1/console/datasets", headers=headers) | |
| assert resp.status_code == 201, resp.text | |
| body = resp.json() | |
| assert body["size"] == 2 | |
| assert body["frozen"] is True | |
| assert body["language_mix"] == {"en": 2} | |
| assert body["source_breakdown"] == {"console": 2} | |
| db = SessionLocal() | |
| try: | |
| items = ( | |
| db.query(DatasetItem) | |
| .filter(DatasetItem.dataset_id == body["id"]) | |
| .all() | |
| ) | |
| assert len(items) == 2 | |
| assert {i.prompt for i in items} == {"keep me", "and me"} | |
| assert {i.response for i in items} == {"echo:keep me", "echo:and me"} | |
| finally: | |
| db.close() | |
| def test_build_dataset_snapshot_does_not_grow_with_later_turns(client, monkeypatch): | |
| headers = _console_user(client, monkeypatch, "explorer6@example.com") | |
| client.post("/v1/console/chat", headers=headers, json={"text": "before"}) | |
| snapshot = client.post("/v1/console/datasets", headers=headers).json() | |
| client.post("/v1/console/chat", headers=headers, json={"text": "after"}) | |
| db = SessionLocal() | |
| try: | |
| items = ( | |
| db.query(DatasetItem).filter(DatasetItem.dataset_id == snapshot["id"]).all() | |
| ) | |
| assert [i.prompt for i in items] == ["before"] | |
| finally: | |
| db.close() | |
| def test_datasets_list_newest_first(client, monkeypatch): | |
| headers = _console_user(client, monkeypatch, "explorer7@example.com") | |
| client.post("/v1/console/chat", headers=headers, json={"text": "hi"}) | |
| first = client.post("/v1/console/datasets", headers=headers).json() | |
| second = client.post("/v1/console/datasets", headers=headers).json() | |
| rows = client.get("/v1/console/datasets", headers=headers).json() | |
| assert [r["id"] for r in rows] == [second["id"], first["id"]] | |
| assert rows[1]["name"] == "Console snapshot 1" | |
| # --------------------------------------------------------------------------- | |
| # Training jobs (read-only until the runner lands in v2) | |
| # --------------------------------------------------------------------------- | |
| def test_training_jobs_empty_by_default(client, monkeypatch): | |
| headers = _console_user(client, monkeypatch, "explorer8@example.com") | |
| assert client.get("/v1/console/training-jobs", headers=headers).json() == [] | |
| def test_training_jobs_resolve_dataset_name(client, monkeypatch): | |
| headers = _console_user(client, monkeypatch, "explorer9@example.com") | |
| client.post("/v1/console/chat", headers=headers, json={"text": "hi"}) | |
| dataset = client.post("/v1/console/datasets", headers=headers).json() | |
| db = SessionLocal() | |
| try: | |
| db.add(TrainingRun(dataset_id=dataset["id"], method="lora", status="queued")) | |
| db.commit() | |
| finally: | |
| db.close() | |
| rows = client.get("/v1/console/training-jobs", headers=headers).json() | |
| assert len(rows) == 1 | |
| assert rows[0]["dataset_id"] == dataset["id"] | |
| assert rows[0]["dataset_name"] == dataset["name"] | |
| assert rows[0]["status"] == "queued" | |
| assert rows[0]["started_at"] is None | |
| def test_dataset_rows_are_visible_to_every_console_user(client, monkeypatch): | |
| monkeypatch.setattr(model_client, "get_reply", _fake_get_reply) | |
| monkeypatch.setenv("CONSOLE_ADMIN_EMAILS", "op-c@example.com,op-d@example.com") | |
| c = _signup(client, "op-c@example.com") | |
| d = _signup(client, "op-d@example.com") | |
| client.post("/v1/console/chat", headers=c, json={"text": "shared corpus"}) | |
| client.post("/v1/console/datasets", headers=c) | |
| rows = client.get("/v1/console/datasets", headers=d).json() | |
| assert len(rows) == 1 | |
| db = SessionLocal() | |
| try: | |
| assert db.query(Dataset).count() == 1 | |
| finally: | |
| db.close() | |