Files
wehub-resource-sync 09e9f3545f
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
CodeQL / Analyze (push) Has been cancelled
dependency-audit / pip-audit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:30:13 +08:00

110 lines
4.7 KiB
Python

"""CLI integration tests for the chat commands (``ask`` + ``history``).
These tests exercise the full CLI -> Client -> RPC path using VCR cassettes,
covering the chat happy paths flagged by issue #1316 (the sibling notebook +
share groups landed in #1322; this file is the chat counterpart tracked by
#1324).
RPC fan-out per command
-----------------------
``ask`` uses the **streamed** chat backend (``_chat.wire`` / a ``_reqid``-bearing
streaming POST), preceded by a ``GET_LAST_CONVERSATION_ID`` lookup:
* ``ask`` -> ``hPTbtc`` (GET_LAST_CONVERSATION_ID) then ``rLM`` (streamed ask).
* ``history`` -> ``hPTbtc`` (GET_LAST_CONVERSATION_ID) then ``khqZz``
(GET_CONVERSATION_TURNS).
The cassettes were recorded against a read-only notebook
(``mock_context`` supplies its full UUID, so ``resolve_notebook_id`` skips the
``LIST_NOTEBOOKS`` preflight and each cassette holds only the chat RPC chain).
Recording (maintainer, with a valid profile)::
NOTEBOOKLM_VCR_RECORD=1 uv run pytest \\
tests/integration/cli_vcr/test_chat.py -m vcr
"""
import json
import pytest
from notebooklm.notebooklm_cli import cli
from ._fixtures import CHAT_NOTEBOOK_ID
from .conftest import CHAT_ANSWER_SCHEMA, assert_json_envelope, notebooklm_vcr, skip_no_cassettes
pytestmark = [pytest.mark.vcr, skip_no_cassettes]
class TestAskCommand:
"""Test ``notebooklm ask`` (streamed chat backend)."""
@notebooklm_vcr.use_cassette("chat_ask.yaml")
def test_ask_question(self, runner, mock_auth_for_vcr, mock_context):
"""``ask`` streams an answer and prints it under the Answer header."""
result = runner.invoke(cli, ["ask", "What is this notebook about?"])
assert result.exit_code == 0, result.output
assert "Answer:" in result.output
@notebooklm_vcr.use_cassette("chat_ask.yaml")
def test_ask_question_json(self, runner, mock_auth_for_vcr, mock_context):
"""``ask --json`` emits the chat-response envelope (answer + references)."""
result = runner.invoke(cli, ["ask", "--json", "What is this notebook about?"])
assert result.exit_code == 0, result.output
# Tier 1 — envelope shape (answer string + references list).
assert_json_envelope(result, schema=CHAT_ANSWER_SCHEMA)
# Parse strictly: a ``--json`` command's whole stdout must be valid
# JSON with no stray prefix.
data = json.loads(result.output)
# ``raw_response`` is deliberately stripped from CLI output for brevity.
assert "raw_response" not in data
class TestHistoryCommand:
"""Test ``notebooklm history`` (GET_LAST_CONVERSATION_ID + GET_CONVERSATION_TURNS)."""
@notebooklm_vcr.use_cassette("chat_get_history.yaml")
def test_history(self, runner, mock_auth_for_vcr, mock_context):
"""``history`` renders the Q&A turns for the last conversation."""
result = runner.invoke(cli, ["history"])
assert result.exit_code == 0, result.output
assert "Conversation History" in result.output
@notebooklm_vcr.use_cassette("chat_get_history.yaml")
def test_history_json(self, runner, mock_auth_for_vcr, mock_context):
"""``history --json`` emits a parseable envelope carrying the turns.
Reuses ``chat_get_history.yaml`` because ``--json`` only changes
rendering, not the underlying ``hPTbtc`` + ``khqZz`` RPC chain, so no
orphan cassette is recorded for an identical request.
"""
result = runner.invoke(cli, ["history", "--json"])
assert result.exit_code == 0, result.output
# Parse strictly: the whole stdout must be valid JSON (no stray prefix).
data = json.loads(result.output)
assert isinstance(data, dict | list), f"Expected JSON, got: {result.output!r}"
class TestGetConversationTurnsCommand:
"""Test conversation turns fetching via GET_CONVERSATION_TURNS (khqZz) RPC.
Cassette: chat_get_conversation_turns.yaml
Notebook: ``CHAT_NOTEBOOK_ID`` (decorative placeholder; see ``_fixtures``).
The cassette captures two sequential batchexecute calls:
1. hPTbtc (GET_LAST_CONVERSATION_ID) -> returns one conversation ID
2. khqZz (GET_CONVERSATION_TURNS) -> returns Q&A turns for that conversation
"""
@notebooklm_vcr.use_cassette("chat_get_conversation_turns.yaml")
def test_history_shows_qa_previews(self, runner, mock_auth_for_vcr, mock_context):
"""history command shows Q&A preview columns populated from khqZz turns API."""
# Use the full UUID directly so resolve_notebook_id skips LIST_NOTEBOOKS
result = runner.invoke(cli, ["history", "-n", CHAT_NOTEBOOK_ID])
assert result.exit_code == 0, result.output
assert "What question should I" in result.output
assert "Based on the sources" in result.output