mirror of
https://github.com/suitenumerique/meet.git
synced 2026-08-31 04:37:57 +00:00
✅(summary) add unit and API tests for summary service
Summary service currently has no tests. Add unit and API tests to summary service.
This commit is contained in:
+3
-1
@@ -57,7 +57,9 @@ It is nice to add information about the purpose of the pull request to help revi
|
|||||||
### Don't forget to:
|
### Don't forget to:
|
||||||
- check your commits
|
- check your commits
|
||||||
- check the linting: `make lint && make frontend-lint`
|
- check the linting: `make lint && make frontend-lint`
|
||||||
- check the tests: `make test`
|
- check the tests:
|
||||||
|
- backend: `make test`
|
||||||
|
- summary: `make test-summary`
|
||||||
- add a changelog entry
|
- add a changelog entry
|
||||||
|
|
||||||
Once all the required tests have passed, you can request a review from the project maintainers.
|
Once all the required tests have passed, you can request a review from the project maintainers.
|
||||||
|
|||||||
@@ -203,6 +203,11 @@ test-back-parallel: ## run all back-end tests in parallel
|
|||||||
bin/pytest -n auto $${args:-${1}}
|
bin/pytest -n auto $${args:-${1}}
|
||||||
.PHONY: test-back-parallel
|
.PHONY: test-back-parallel
|
||||||
|
|
||||||
|
test-summary: ## run summary service tests
|
||||||
|
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||||
|
bin/pytest-summary $${args:-${1}}
|
||||||
|
.PHONY: test-summary
|
||||||
|
|
||||||
makemigrations: ## run django makemigrations for the Meet project.
|
makemigrations: ## run django makemigrations for the Meet project.
|
||||||
@echo "$(BOLD)Running makemigrations$(RESET)"
|
@echo "$(BOLD)Running makemigrations$(RESET)"
|
||||||
@$(COMPOSE) up -d postgresql
|
@$(COMPOSE) up -d postgresql
|
||||||
|
|||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# NB: this file is used locally only. In CI, it is overwritten by pytest install
|
||||||
|
|
||||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||||
|
|
||||||
_dc_run \
|
_dc_run \
|
||||||
-e DJANGO_CONFIGURATION=Test \
|
-e DJANGO_CONFIGURATION=Test \
|
||||||
app-dev \
|
app-dev \
|
||||||
pytest "$@"
|
pytest "$@"
|
||||||
|
|||||||
Executable
+7
@@ -0,0 +1,7 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||||
|
|
||||||
|
_dc_run \
|
||||||
|
app-summary-dev \
|
||||||
|
python -m pytest "$@"
|
||||||
@@ -21,8 +21,17 @@ dependencies = [
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
"ruff==0.14.4",
|
"ruff==0.14.4",
|
||||||
|
"pytest==9.0.2",
|
||||||
|
"responses>=0.25.8",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
markers = [
|
||||||
|
"unit: Test individual components",
|
||||||
|
"integration: Test the API",
|
||||||
|
]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=61.0"]
|
requires = ["setuptools>=61.0"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
@@ -49,11 +58,10 @@ select = [
|
|||||||
"T20", # flake8-print
|
"T20", # flake8-print
|
||||||
"W", # pycodestyle warning
|
"W", # pycodestyle warning
|
||||||
]
|
]
|
||||||
|
ignore= ["DJ001", "PLR2004"]
|
||||||
|
|
||||||
[tool.ruff.lint.per-file-ignores]
|
[tool.ruff.lint.per-file-ignores]
|
||||||
"tests/*" = [
|
"tests/*" = ["S", "SLF"]
|
||||||
"S101", # use of assert
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.ruff.lint.pydocstyle]
|
[tool.ruff.lint.pydocstyle]
|
||||||
# Use Google-style docstrings.
|
# Use Google-style docstrings.
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ def format_transcript(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def format_actions(llm_output: dict) -> str:
|
def _format_actions(llm_output: dict) -> str:
|
||||||
"""Format the actions from the LLM output into a markdown list.
|
"""Format the actions from the LLM output into a markdown list.
|
||||||
|
|
||||||
fomat:
|
fomat:
|
||||||
@@ -328,7 +328,7 @@ def summarize_transcription(
|
|||||||
response_format=FORMAT_NEXT_STEPS,
|
response_format=FORMAT_NEXT_STEPS,
|
||||||
)
|
)
|
||||||
|
|
||||||
next_steps = format_actions(json.loads(next_steps))
|
next_steps = _format_actions(json.loads(next_steps))
|
||||||
|
|
||||||
logger.info("Next steps generated")
|
logger.info("Next steps generated")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Tests for the summary service."""
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""Shared test fixtures and environment setup for the summary service tests."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Set required environment variables before any summary module imports.
|
||||||
|
# This is necessary because several modules call get_settings() at module level,
|
||||||
|
# which validates env vars via Pydantic Settings.
|
||||||
|
os.environ.setdefault("APP_API_TOKEN", "test-api-token")
|
||||||
|
os.environ.setdefault("AWS_STORAGE_BUCKET_NAME", "test-bucket")
|
||||||
|
os.environ.setdefault("AWS_S3_ENDPOINT_URL", "http://localhost:9000")
|
||||||
|
os.environ.setdefault("AWS_S3_ACCESS_KEY_ID", "test-access-key")
|
||||||
|
os.environ.setdefault("AWS_S3_SECRET_ACCESS_KEY", "test-secret-key")
|
||||||
|
os.environ.setdefault("AWS_S3_SECURE_ACCESS", "false")
|
||||||
|
os.environ.setdefault("WHISPERX_API_KEY", "test-whisperx-key")
|
||||||
|
os.environ.setdefault("WHISPERX_BASE_URL", "http://localhost:8000/v1")
|
||||||
|
os.environ.setdefault("LLM_BASE_URL", "http://localhost:8001/v1")
|
||||||
|
os.environ.setdefault("LLM_API_KEY", "test-llm-key")
|
||||||
|
os.environ.setdefault("LLM_MODEL", "test-model")
|
||||||
|
os.environ.setdefault("WEBHOOK_API_TOKEN", "test-webhook-token")
|
||||||
|
os.environ.setdefault("WEBHOOK_URL", "http://localhost:8002/webhook")
|
||||||
|
os.environ.setdefault("CELERY_BROKER_URL", "memory://")
|
||||||
|
os.environ.setdefault("CELERY_RESULT_BACKEND", "cache+memory://")
|
||||||
|
os.environ.setdefault("POSTHOG_ENABLED", "false")
|
||||||
|
os.environ.setdefault("SENTRY_IS_ENABLED", "false")
|
||||||
|
os.environ.setdefault("LANGFUSE_ENABLED", "false")
|
||||||
|
os.environ.setdefault("TASK_TRACKER_REDIS_URL", "redis://localhost:6379/0")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Integration tests for the summary service."""
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"""Integration test configuration. Provides shared fixtures."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from summary.core.celery_worker import celery
|
||||||
|
from summary.main import app
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def client():
|
||||||
|
"""Provide a FastAPI TestClient for integration tests."""
|
||||||
|
return TestClient(app)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def eager_celery():
|
||||||
|
"""Run Celery tasks synchronously in the same process."""
|
||||||
|
celery.conf.task_always_eager = True
|
||||||
|
celery.conf.task_eager_propagates = True
|
||||||
|
yield
|
||||||
|
celery.conf.task_always_eager = False
|
||||||
|
celery.conf.task_eager_propagates = False
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
"""Integration tests for the health check endpoints."""
|
||||||
|
|
||||||
|
|
||||||
|
class TestHeartbeat:
|
||||||
|
"""Tests for the /__heartbeat__ endpoint."""
|
||||||
|
|
||||||
|
def test_returns_200(self, client):
|
||||||
|
"""The heartbeat endpoint responds with 200 OK without a token."""
|
||||||
|
response = client.get("/__heartbeat__")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
class TestLBHeartbeat:
|
||||||
|
"""Tests for the /__lbheartbeat__ endpoint."""
|
||||||
|
|
||||||
|
def test_returns_200(self, client):
|
||||||
|
"""The load-balancer heartbeat endpoint responds with 200 OK without a token."""
|
||||||
|
response = client.get("/__lbheartbeat__")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
"""Integration test for the transcribe-and-summarize task flow via the API."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import responses
|
||||||
|
|
||||||
|
from summary.core.config import get_settings
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
API_PREFIX = "/api/v1"
|
||||||
|
AUTH_HEADER = {"Authorization": f"Bearer {settings.app_api_token.get_secret_value()}"}
|
||||||
|
WEBHOOK_URL = settings.webhook_url
|
||||||
|
|
||||||
|
|
||||||
|
class TestTranscribeSummarizeFlow:
|
||||||
|
"""End-to-end test: POST /tasks/ triggers transcription and summary via webhook."""
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
@patch("summary.core.celery_worker.analytics")
|
||||||
|
@patch("summary.core.celery_worker.LLMObservability")
|
||||||
|
@patch("summary.core.celery_worker.LLMService")
|
||||||
|
@patch("summary.core.celery_worker.metadata_manager")
|
||||||
|
@patch("summary.core.celery_worker.openai")
|
||||||
|
@patch("summary.core.celery_worker.file_service")
|
||||||
|
def test_transcription_and_summary_are_submitted( # noqa: PLR0913
|
||||||
|
self,
|
||||||
|
mock_file_service,
|
||||||
|
mock_openai,
|
||||||
|
mock_metadata,
|
||||||
|
mock_llm_cls,
|
||||||
|
mock_observability_cls,
|
||||||
|
mock_analytics,
|
||||||
|
client,
|
||||||
|
eager_celery,
|
||||||
|
):
|
||||||
|
"""Creating a task produces a transcription and summary sent to the webhook."""
|
||||||
|
# Stub file service
|
||||||
|
fake_audio = MagicMock()
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def fake_prepare(filename):
|
||||||
|
yield fake_audio, {"duration": 60.0}
|
||||||
|
|
||||||
|
mock_file_service.prepare_audio_file = fake_prepare
|
||||||
|
|
||||||
|
# Stub WhisperX transcription
|
||||||
|
fake_transcription = SimpleNamespace(
|
||||||
|
segments=[
|
||||||
|
{"speaker": "SPEAKER_00", "text": "Hello everyone."},
|
||||||
|
{"speaker": "SPEAKER_01", "text": "Let's discuss the roadmap."},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.audio.transcriptions.create.return_value = fake_transcription
|
||||||
|
mock_openai.OpenAI.return_value = mock_client
|
||||||
|
|
||||||
|
# Stub analytics to enable summary
|
||||||
|
mock_analytics.is_feature_enabled.return_value = True
|
||||||
|
|
||||||
|
# Stub LLM for summarization
|
||||||
|
mock_llm = MagicMock()
|
||||||
|
mock_llm_cls.return_value = mock_llm
|
||||||
|
|
||||||
|
plan_json = json.dumps({"titles": ["Roadmap"]})
|
||||||
|
next_steps_json = json.dumps(
|
||||||
|
{
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"title": "Draft roadmap",
|
||||||
|
"assignees": ["Aleb"],
|
||||||
|
"due_date": "2026-03-04",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
mock_llm.call.side_effect = [
|
||||||
|
"### TL;DR\nQuick overview.", # tldr
|
||||||
|
plan_json, # parts plan
|
||||||
|
"### Roadmap\nDetails.", # part content
|
||||||
|
next_steps_json, # next steps
|
||||||
|
"Cleaned summary.", # cleaning
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_observability_cls.return_value = MagicMock()
|
||||||
|
|
||||||
|
# Stub webhook (called twice: transcription + summary)
|
||||||
|
responses.post(WEBHOOK_URL, json={"id": "doc-1"}, status=200)
|
||||||
|
responses.post(WEBHOOK_URL, json={"id": "doc-2"}, status=200)
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"owner_id": "owner-1",
|
||||||
|
"filename": "recording.webm",
|
||||||
|
"email": "user@example.com",
|
||||||
|
"sub": "user-sub-id",
|
||||||
|
"room": "Visio room",
|
||||||
|
"recording_date": "2026-03-04",
|
||||||
|
"recording_time": "09:00",
|
||||||
|
"language": "en",
|
||||||
|
"download_link": "https://example.com/rec.webm",
|
||||||
|
"context_language": "en",
|
||||||
|
}
|
||||||
|
|
||||||
|
response = client.post(
|
||||||
|
f"{API_PREFIX}/tasks/", json=payload, headers=AUTH_HEADER
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
body = response.json()
|
||||||
|
assert "id" in body
|
||||||
|
assert body["message"] == "Task created"
|
||||||
|
|
||||||
|
# Verify the webhook received the transcription
|
||||||
|
assert len(responses.calls) >= 1
|
||||||
|
transcript_payload = json.loads(responses.calls[0].request.body)
|
||||||
|
assert "SPEAKER_00" in transcript_payload["content"]
|
||||||
|
assert "Hello everyone." in transcript_payload["content"]
|
||||||
|
assert "Visio room" in transcript_payload["title"]
|
||||||
|
assert transcript_payload["email"] == "user@example.com"
|
||||||
|
assert transcript_payload["sub"] == "user-sub-id"
|
||||||
|
|
||||||
|
# Verify the webhook received the summary
|
||||||
|
assert len(responses.calls) == 2
|
||||||
|
summary_payload = json.loads(responses.calls[1].request.body)
|
||||||
|
assert "TL;DR" in summary_payload["content"]
|
||||||
|
assert "Cleaned summary." in summary_payload["content"]
|
||||||
|
assert "Draft roadmap" in summary_payload["content"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Unit tests for the summary service."""
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
"""Tests for the celery_worker module."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import responses
|
||||||
|
|
||||||
|
from summary.core.celery_worker import (
|
||||||
|
format_transcript,
|
||||||
|
summarize_transcription,
|
||||||
|
transcribe_audio,
|
||||||
|
)
|
||||||
|
from summary.core.config import get_settings
|
||||||
|
from summary.core.file_service import FileServiceException
|
||||||
|
|
||||||
|
settings = get_settings()
|
||||||
|
|
||||||
|
WEBHOOK_URL = settings.webhook_url
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# transcribe_audio
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestTranscribeAudio:
|
||||||
|
"""Tests for the transcribe_audio function."""
|
||||||
|
|
||||||
|
@patch("summary.core.celery_worker.metadata_manager")
|
||||||
|
@patch("summary.core.celery_worker.openai")
|
||||||
|
@patch("summary.core.celery_worker.file_service")
|
||||||
|
def test_success(self, mock_file_service, mock_openai, mock_metadata):
|
||||||
|
"""Transcription succeeds and returns the transcription object."""
|
||||||
|
fake_audio = MagicMock()
|
||||||
|
fake_metadata = {"duration": 120.5}
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def fake_prepare(filename):
|
||||||
|
yield fake_audio, fake_metadata
|
||||||
|
|
||||||
|
mock_file_service.prepare_audio_file = fake_prepare
|
||||||
|
|
||||||
|
fake_transcription = SimpleNamespace(
|
||||||
|
segments=[{"speaker": "SPEAKER_00", "text": "Hello"}],
|
||||||
|
)
|
||||||
|
mock_client = MagicMock()
|
||||||
|
mock_client.audio.transcriptions.create.return_value = fake_transcription
|
||||||
|
mock_openai.OpenAI.return_value = mock_client
|
||||||
|
|
||||||
|
result = transcribe_audio("task-1", "recording.ogg", "en")
|
||||||
|
|
||||||
|
assert result is fake_transcription
|
||||||
|
mock_client.audio.transcriptions.create.assert_called_once()
|
||||||
|
call_kwargs = mock_client.audio.transcriptions.create.call_args
|
||||||
|
assert call_kwargs.kwargs["language"] == "en"
|
||||||
|
assert call_kwargs.kwargs["file"] is fake_audio
|
||||||
|
|
||||||
|
@patch("summary.core.celery_worker.metadata_manager")
|
||||||
|
@patch("summary.core.celery_worker.openai")
|
||||||
|
@patch("summary.core.celery_worker.file_service")
|
||||||
|
def test_file_service_error_returns_none(
|
||||||
|
self, mock_file_service, mock_openai, mock_metadata
|
||||||
|
):
|
||||||
|
"""Returns None when the file cannot be retrieved."""
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def failing_prepare(filename):
|
||||||
|
raise FileServiceException("download failed")
|
||||||
|
yield # NOSONAR - yield required for contextmanager
|
||||||
|
|
||||||
|
mock_file_service.prepare_audio_file = failing_prepare
|
||||||
|
|
||||||
|
result = transcribe_audio("task-1", "recording.ogg", "en")
|
||||||
|
|
||||||
|
assert result is None
|
||||||
|
mock_openai.OpenAI.return_value.audio.transcriptions.create.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# format_transcript
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestFormatTranscript:
|
||||||
|
"""Tests for the format_transcript function."""
|
||||||
|
|
||||||
|
def test_with_segments(self):
|
||||||
|
"""Formats a transcription with segments into content and title."""
|
||||||
|
transcription = {
|
||||||
|
"segments": [
|
||||||
|
{"speaker": "SPEAKER_00", "text": "Hello everyone."},
|
||||||
|
{"speaker": "SPEAKER_01", "text": "Good morning."},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
content, title = format_transcript(
|
||||||
|
transcription,
|
||||||
|
context_language="en",
|
||||||
|
language="en",
|
||||||
|
room="Daily standup",
|
||||||
|
recording_date="2026-03-04",
|
||||||
|
recording_time="09:00",
|
||||||
|
download_link="https://example.com/rec.ogg",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "SPEAKER_00" in content
|
||||||
|
assert "Hello everyone." in content
|
||||||
|
assert "SPEAKER_01" in content
|
||||||
|
assert "Good morning." in content
|
||||||
|
assert "Daily standup" in title
|
||||||
|
assert "2026-03-04" in title
|
||||||
|
assert "09:00" in title
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"context_language, expected_string",
|
||||||
|
[
|
||||||
|
("en", "Download your recording"),
|
||||||
|
("fr", "Télécharger votre enregistrement"),
|
||||||
|
("de", "diesem Link folgen"),
|
||||||
|
("nl", "Download uw opname door"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_context_language(self, context_language, expected_string):
|
||||||
|
"""Context language parameter modifies output."""
|
||||||
|
transcription = {
|
||||||
|
"segments": [
|
||||||
|
{"speaker": "SPEAKER_00", "text": "Hello everyone."},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
content, _ = format_transcript(
|
||||||
|
transcription,
|
||||||
|
context_language=context_language,
|
||||||
|
language="en",
|
||||||
|
room="Daily standup",
|
||||||
|
recording_date="2026-03-04",
|
||||||
|
recording_time="09:00",
|
||||||
|
download_link="https://example.com/rec.ogg",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert expected_string in content
|
||||||
|
|
||||||
|
def test_empty_segments(self):
|
||||||
|
"""Returns empty-transcription message when there are no segments."""
|
||||||
|
transcription = {"segments": []}
|
||||||
|
|
||||||
|
content, title = format_transcript(
|
||||||
|
transcription,
|
||||||
|
context_language="en",
|
||||||
|
language="en",
|
||||||
|
room=None,
|
||||||
|
recording_date=None,
|
||||||
|
recording_time=None,
|
||||||
|
download_link=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "No audio content" in content or "Transcription" in title
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# summarize_transcription
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestSummarizeTranscription:
|
||||||
|
"""Tests for the summarize_transcription Celery task."""
|
||||||
|
|
||||||
|
@responses.activate
|
||||||
|
@patch("summary.core.celery_worker.LLMService")
|
||||||
|
@patch("summary.core.celery_worker.LLMObservability")
|
||||||
|
@patch("summary.core.celery_worker.analytics")
|
||||||
|
def test_generates_and_submits_summary(
|
||||||
|
self, mock_analytics, mock_observability_cls, mock_llm_cls
|
||||||
|
):
|
||||||
|
"""Assembles TLDR + parts + next steps + cleaning, then submits."""
|
||||||
|
mock_analytics.is_feature_enabled.return_value = False
|
||||||
|
|
||||||
|
# Mock the webhook HTTP endpoint
|
||||||
|
responses.post(
|
||||||
|
WEBHOOK_URL,
|
||||||
|
json={"id": "doc-42"},
|
||||||
|
status=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_llm = MagicMock()
|
||||||
|
mock_llm_cls.return_value = mock_llm
|
||||||
|
|
||||||
|
plan_json = json.dumps({"titles": ["Topic A", "Topic B"]})
|
||||||
|
next_steps_json = json.dumps(
|
||||||
|
{
|
||||||
|
"actions": [
|
||||||
|
{
|
||||||
|
"title": "What's nice about Visio",
|
||||||
|
"assignees": ["Aleb"],
|
||||||
|
"due_date": "2026-03-04",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# LLM calls in order: tldr, parts (plan), part A, part B, next-steps, cleaning
|
||||||
|
mock_llm.call.side_effect = [
|
||||||
|
"### TL;DR\nShort summary.", # tldr
|
||||||
|
plan_json, # parts plan
|
||||||
|
"### Topic A\nDetails about A.", # part A
|
||||||
|
"### Topic B\nDetails about B.", # part B
|
||||||
|
next_steps_json, # next steps
|
||||||
|
"Cleaned summary content.", # cleaning
|
||||||
|
]
|
||||||
|
|
||||||
|
mock_observability = MagicMock()
|
||||||
|
mock_observability_cls.return_value = mock_observability
|
||||||
|
|
||||||
|
# Push a fake request context so self.request.id is available
|
||||||
|
summarize_transcription.push_request(id="summary-task-1")
|
||||||
|
try:
|
||||||
|
summarize_transcription.run(
|
||||||
|
"owner-1",
|
||||||
|
"Full transcript text",
|
||||||
|
"user@example.com",
|
||||||
|
"oidc-sub-123",
|
||||||
|
"99.999% uptime. Is it reasonable ?",
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
summarize_transcription.pop_request()
|
||||||
|
|
||||||
|
# Verify the webhook was called with the assembled summary
|
||||||
|
assert len(responses.calls) == 1
|
||||||
|
webhook_request = responses.calls[0]
|
||||||
|
submitted_payload = json.loads(webhook_request.request.body)
|
||||||
|
|
||||||
|
assert "TL;DR" in submitted_payload["content"]
|
||||||
|
assert "Cleaned summary content." in submitted_payload["content"]
|
||||||
|
assert "What's nice about Visio" in submitted_payload["content"]
|
||||||
|
assert "99.999% uptime. Is it reasonable ?" in submitted_payload["title"]
|
||||||
|
assert submitted_payload["email"] == "user@example.com"
|
||||||
|
assert submitted_payload["sub"] == "oidc-sub-123"
|
||||||
|
|
||||||
|
# Verify auth header was sent
|
||||||
|
assert (
|
||||||
|
webhook_request.request.headers["Authorization"]
|
||||||
|
== f"Bearer {settings.webhook_api_token.get_secret_value()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# LLM was called for: tldr, plan, part A, part B, next-steps, cleaning
|
||||||
|
expected_llm_calls = 6
|
||||||
|
assert mock_llm.call.call_count == expected_llm_calls
|
||||||
|
|
||||||
|
mock_observability.flush.assert_called_once()
|
||||||
Reference in New Issue
Block a user