sprint: fixes batch — saveNote history, DeepSeek retry, channel rotation, test fixes, docker env, blocking transcript, sync errors

This commit is contained in:
Gan, Jimmy
2026-06-28 17:08:32 +08:00
parent 5c641903c9
commit 77c6a9d963
6 changed files with 285 additions and 131 deletions
+54 -28
View File
@@ -1,3 +1,4 @@
"""Tests for summarizer.py — mocks transcript API + DeepSeek API."""
import pytest
import os
import httpx
@@ -5,6 +6,7 @@ import aiosqlite
import summarizer
from unittest.mock import patch, MagicMock, AsyncMock
class MockTranscript:
def fetch(self):
return [
@@ -25,6 +27,17 @@ class MockTranscriptList:
def __iter__(self):
return iter([self.transcript])
def _mock_deepseek_response(content: str) -> MagicMock:
"""Build a DeepSeek/OpenAI-compatible API response."""
r = MagicMock(spec=httpx.Response)
r.status_code = 200
r.json.return_value = {
"choices": [{"message": {"content": content}}]
}
return r
@pytest.mark.asyncio
async def test_download_transcript_success():
mock_trans = MockTranscript()
@@ -34,9 +47,9 @@ async def test_download_transcript_success():
text = summarizer.download_transcript("test_vid")
assert text == "Welcome to this video tutorial. Today we are learning about Svelte 5 and runes."
@pytest.mark.asyncio
async def test_summarize_video_success(test_db):
# Insert a pending video in db first
await test_db.execute("""
INSERT INTO videos (video_id, title, url, status)
VALUES ('summarize_vid', 'Video to Summarize', 'https://www.youtube.com/watch?v=summarize_vid', 'pending')
@@ -45,19 +58,8 @@ async def test_summarize_video_success(test_db):
mock_trans = MockTranscript()
mock_list = MockTranscriptList(mock_trans)
mock_response = _mock_deepseek_response("**Mock Core Hook**\n- Takeaway 1\n- Takeaway 2\nVerdict line.")
# Mock Claude HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.json.return_value = {
"content": [
{
"text": "**Mock Core Hook**\n- Takeaway 1\n- Takeaway 2\nVerdict line."
}
]
}
# Patch both transcript api and httpx AsyncClient post call
with patch("youtube_transcript_api.YouTubeTranscriptApi.list", return_value=mock_list), \
patch("httpx.AsyncClient.post", new_callable=AsyncMock, return_value=mock_response) as mock_post:
@@ -69,12 +71,12 @@ async def test_summarize_video_success(test_db):
assert summarizer.BASE_URL in url
headers = mock_post.call_args[1]["headers"]
assert "x-api-key" in headers
assert "Authorization" in headers
assert headers["User-Agent"] is not None
payload = mock_post.call_args[1]["json"]
assert payload["model"] == "claude-haiku-4-5-20251001"
assert len(payload["messages"]) == 1
assert payload["model"] == "deepseek-chat"
assert len(payload["messages"]) == 2 # system + user
assert summary == "**Mock Core Hook**\n- Takeaway 1\n- Takeaway 2\nVerdict line."
@@ -85,9 +87,40 @@ async def test_summarize_video_success(test_db):
assert video["transcript"] == "Welcome to this video tutorial. Today we are learning about Svelte 5 and runes."
assert video["summary"] == "**Mock Core Hook**\n- Takeaway 1\n- Takeaway 2\nVerdict line."
@pytest.mark.asyncio
async def test_summarize_video_deepseek_error_leaves_null(test_db):
"""When DeepSeek fails, summary should stay NULL for retry."""
await test_db.execute("""
INSERT INTO videos (video_id, title, url, status)
VALUES ('err_vid', 'Error Video', 'https://youtube.com/watch?v=err_vid', 'pending')
""")
await test_db.commit()
mock_trans = MockTranscript()
mock_list = MockTranscriptList(mock_trans)
mock_response = MagicMock(spec=httpx.Response)
mock_response.status_code = 429
# raise_for_status on 429 should raise HTTPStatusError
mock_response.raise_for_status.side_effect = httpx.HTTPStatusError(
"429 Rate Limited", request=MagicMock(), response=mock_response
)
with patch("youtube_transcript_api.YouTubeTranscriptApi.list", return_value=mock_list), \
patch("httpx.AsyncClient.post", new_callable=AsyncMock, return_value=mock_response):
result = await summarizer.summarize_video("err_vid")
assert result is None # Returns None instead of an error string
# Verify DB — summary should remain NULL
cursor = await test_db.execute("SELECT summary FROM videos WHERE video_id = 'err_vid'")
row = await cursor.fetchone()
assert row["summary"] is None
@pytest.mark.asyncio
async def test_summarize_all_pending(test_db):
# Insert multiple videos, one pending and one already summarized
await test_db.execute("""
INSERT INTO videos (video_id, title, url, summary, status)
VALUES
@@ -98,28 +131,21 @@ async def test_summarize_all_pending(test_db):
mock_trans = MockTranscript()
mock_list = MockTranscriptList(mock_trans)
mock_response = MagicMock(spec=httpx.Response)
mock_response.status_code = 200
mock_response.json.return_value = {
"content": [{"text": "Fresh summary"}]
}
mock_response = _mock_deepseek_response("Fresh summary")
with patch("youtube_transcript_api.YouTubeTranscriptApi.list", return_value=mock_list), \
patch("httpx.AsyncClient.post", new_callable=AsyncMock, return_value=mock_response) as mock_post:
await summarizer.summarize_all_pending()
# Should only call API once for 'pending_vid'
# Should only call API once for 'pending_vid' (done_vid already has summary)
mock_post.assert_called_once()
payload = mock_post.call_args[1]["json"]
assert "Welcome to this video tutorial." in payload["messages"][0]["content"]
# Verify db states
cursor = await test_db.execute("SELECT video_id, summary FROM videos ORDER BY video_id")
videos = await cursor.fetchall()
assert videos[0]["video_id"] == "done_vid"
assert videos[0]["summary"] == "Existing summary" # Untouched
assert videos[0]["summary"] == "Existing summary" # Untouched
assert videos[1]["video_id"] == "pending_vid"
assert videos[1]["summary"] == "Fresh summary" # Updated
assert videos[1]["summary"] == "Fresh summary" # Updated