09e9f3545f
CodeQL / Analyze (push) Waiting to run
dependency-audit / pip-audit (push) Waiting to run
Test / Code Quality (push) Has been cancelled
Test / Test (macos-latest, Python 3.10) (push) Has been cancelled
Test / Test (macos-latest, Python 3.11) (push) Has been cancelled
Test / Test (macos-latest, Python 3.12) (push) Has been cancelled
Test / Test (macos-latest, Python 3.13) (push) Has been cancelled
Test / Test (macos-latest, Python 3.14) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.10) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.11) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.12) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.13) (push) Has been cancelled
Test / Test (ubuntu-latest, Python 3.14) (push) Has been cancelled
Test / Test (windows-latest, Python 3.10) (push) Has been cancelled
Test / Test (windows-latest, Python 3.11) (push) Has been cancelled
Test / Test (windows-latest, Python 3.12) (push) Has been cancelled
Test / Test (windows-latest, Python 3.13) (push) Has been cancelled
Test / Test (windows-latest, Python 3.14) (push) Has been cancelled
127 lines
5.0 KiB
Python
127 lines
5.0 KiB
Python
"""U6: POST /v1/notebooks/{id}/chat — blocking ask."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
from notebooklm.exceptions import RateLimitError
|
|
|
|
from .fakes import FakeClient
|
|
|
|
|
|
def test_ask_returns_full_answer(authed_client: TestClient) -> None:
|
|
resp = authed_client.post("/v1/notebooks/nb-1/chat", json={"question": "What is X?"})
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["answer"] == "answer to: What is X?"
|
|
assert body["conversation_id"] == "conv-1"
|
|
assert "references" in body
|
|
|
|
|
|
def test_ask_strips_raw_response_debug_blob(authed_client: TestClient) -> None:
|
|
"""The internal ``raw_response`` debug blob is never serialized (shared view)."""
|
|
resp = authed_client.post("/v1/notebooks/nb-1/chat", json={"question": "What is X?"})
|
|
assert resp.status_code == 200
|
|
assert "raw_response" not in resp.json()
|
|
|
|
|
|
def test_conversation_id_is_forwarded(authed_client: TestClient, fake_client: FakeClient) -> None:
|
|
resp = authed_client.post(
|
|
"/v1/notebooks/nb-1/chat",
|
|
json={"question": "follow up", "conversation_id": "conv-42"},
|
|
)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["conversation_id"] == "conv-42"
|
|
assert fake_client.last_ask == {"notebook_id": "nb-1", "conversation_id": "conv-42"}
|
|
|
|
|
|
def test_rate_limited_ask_is_429(authed_client: TestClient, fake_client: FakeClient) -> None:
|
|
fake_client.chat_error = RateLimitError("slow down")
|
|
resp = authed_client.post("/v1/notebooks/nb-1/chat", json={"question": "hi"})
|
|
assert resp.status_code == 429
|
|
assert resp.json()["error"]["category"] == "rate_limited"
|
|
|
|
|
|
def test_unauthorized_is_401(raw_client: TestClient) -> None:
|
|
resp = raw_client.post(
|
|
"/v1/notebooks/nb-1/chat", json={"question": "hi"}, headers={"Host": "127.0.0.1"}
|
|
)
|
|
assert resp.status_code == 401
|
|
|
|
|
|
# --- POST /v1/notebooks/{id}/chat/configure ---------------------------------
|
|
|
|
|
|
def test_configure_preset_writes_mode(authed_client: TestClient, fake_client: FakeClient) -> None:
|
|
resp = authed_client.post(
|
|
"/v1/notebooks/nb-1/chat/configure", json={"chat_mode": "learning-guide"}
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["status"] == "configured"
|
|
assert body["mode"] == "learning-guide"
|
|
# A preset short-circuits to set_mode (not the custom configure path).
|
|
assert fake_client.last_configure is not None
|
|
assert fake_client.last_configure["notebook_id"] == "nb-1"
|
|
assert "mode" in fake_client.last_configure
|
|
|
|
|
|
def test_configure_custom_writes_goal_and_length(
|
|
authed_client: TestClient, fake_client: FakeClient
|
|
) -> None:
|
|
resp = authed_client.post(
|
|
"/v1/notebooks/nb-1/chat/configure",
|
|
json={"goal": "Explain like I'm five", "response_length": "shorter"},
|
|
)
|
|
assert resp.status_code == 200
|
|
body = resp.json()
|
|
assert body["status"] == "configured"
|
|
assert body["mode"] is None
|
|
# A non-empty goal selects the CUSTOM chat goal.
|
|
assert body["goal_name"] == "custom"
|
|
assert body["persona"] == "Explain like I'm five"
|
|
assert body["response_length"] == "shorter"
|
|
assert fake_client.last_configure is not None
|
|
assert fake_client.last_configure["custom_prompt"] == "Explain like I'm five"
|
|
|
|
|
|
def test_configure_partial_merges_preserving_omitted_field(
|
|
authed_client: TestClient, fake_client: FakeClient
|
|
) -> None:
|
|
"""A length-only REST configure merges: the current CUSTOM goal+persona is preserved (#1751)."""
|
|
from notebooklm.rpc.types import ChatGoal, ChatResponseLength
|
|
from notebooklm.types import ChatSettings
|
|
|
|
fake_client.chat_settings = ChatSettings(
|
|
goal=ChatGoal.CUSTOM,
|
|
response_length=ChatResponseLength.DEFAULT,
|
|
custom_prompt="keep this persona",
|
|
)
|
|
resp = authed_client.post(
|
|
"/v1/notebooks/nb-1/chat/configure",
|
|
json={"response_length": "longer"},
|
|
)
|
|
assert resp.status_code == 200
|
|
# The read-modify-write read happened, and the omitted goal/persona is preserved.
|
|
assert fake_client.last_get_settings == "nb-1"
|
|
assert fake_client.last_configure is not None
|
|
assert fake_client.last_configure["goal"] == ChatGoal.CUSTOM
|
|
assert fake_client.last_configure["response_length"] == ChatResponseLength.LONGER
|
|
assert fake_client.last_configure["custom_prompt"] == "keep this persona"
|
|
|
|
|
|
def test_configure_preset_and_custom_conflict_is_400(authed_client: TestClient) -> None:
|
|
resp = authed_client.post(
|
|
"/v1/notebooks/nb-1/chat/configure",
|
|
json={"chat_mode": "concise", "response_length": "longer"},
|
|
)
|
|
assert resp.status_code == 400
|
|
assert resp.json()["error"]["category"] == "validation"
|
|
|
|
|
|
def test_configure_unknown_mode_is_422(authed_client: TestClient) -> None:
|
|
# ``chat_mode`` is a Literal, so an out-of-enum value is rejected at the
|
|
# request-schema boundary (422), not reaching the core.
|
|
resp = authed_client.post("/v1/notebooks/nb-1/chat/configure", json={"chat_mode": "bogus"})
|
|
assert resp.status_code == 422
|