"""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()