Add offline unit test suite for chat_backend (#12)
Unit Tests / test (push) Successful in 9s
Unit Tests / test (push) Successful in 9s
Closes #5 ## Summary - Replaces the three scattered test modules (`chat_backend/tests.py`, `services/tests.py`, `services/prompt_classifier/tests.py`) with a `chat_backend/tests/` package: **242 deterministic tests plus 6 opt-in live-Ollama checks**, up from 10 tests (3 of which were skipped and 4 of which were never even discovered). - The suite runs fully offline — no Ollama, Chroma, SMTP or network access. LangChain runnables are replaced by a small `FakeChain`, Chroma/embeddings are mocked, email uses Django's locmem backend, and blobs go through `DatabaseStorage`. - New `llm_be/test_runner.py` (wired via `TEST_RUNNER`) sets `SKIP_RAG_INIT=1` and an MD5 password hasher, so the suite cannot accidentally reach a model server and finishes in ~5s on SQLite (~17s on Postgres) instead of ~30s. ## Coverage | Area | File | |------|------| | `TimeInfoBase.save`, slugs, `get_duration`, `file_exists`, cascades | `test_models.py` | | `DatabaseStorage` save/open/exists/size/listdir/delete/times | `test_storage.py` | | JWT claim, prompt/user/feedback/document serializers | `test_serializers.py` | | auth + token, invite, feedback, company users, set-password, TOS | `test_views_users.py` | | conversation list/order/create/detail/soft-delete | `test_views_conversations.py` | | all four analytics endpoints, including empty-month behaviour | `test_views_analytics.py` | | workspace + document upload/list/detail, 404 and 400 paths | `test_views_documents.py` | | prompt classifier rules/parsing, moderation fail-safe, title cleanup | `test_services_classifiers.py` | | CSV/XLSX/DOCX/PDF analysis, plot generation, error payloads | `test_services_data_analysis.py` | | loader selection, filename sanitising, ingest, temp-file cleanup, search filters | `test_services_rag.py` | | history formatting and streaming | `test_services_llm.py` | | document re-index on create/delete, `SKIP_RAG_INIT` guard | `test_signals.py` | | consumer DB helpers, LangGraph nodes (moderation, classification, generation, search flags), websocket routes | `test_consumers.py` | Live checks (non-deterministic, need a model server): ```bash cd llm_be RUN_LIVE_OLLAMA_TESTS=1 uv run python manage.py test chat_backend.tests.test_live_ollama ``` ## Bugs the tests surfaced (fixed here) 1. **`ConversationDetailView.post` silently dropped every prompt.** `import datetime` shadowed `from datetime import datetime`, so `datetime.now()` raised `AttributeError` inside a bare `except` and the endpoint returned 200 without saving. Now uses `timezone.now()`. 2. **Prompt attachments never reached the LLM.** `get_conversation_file_async` (both consumers) did `sync_to_async(prompt.file.read)` — with `DatabaseStorage` the attribute access itself opens the blob, i.e. a DB query in async context, raising `SynchronousOnlyOperation` that was swallowed and returned `(None, None)`. The read now happens inside the thread. 3. **`DatabaseStorage._save` crashed on a str-backed `ContentFile`** (`TypeError: sequence item 0: expected a bytes-like object`); chunks are encoded when needed. 4. **`services/prompt_classifier/__init__.,py`** (note the comma) meant the directory was only an implicit namespace package, which is why its test module was never collected. Renamed, and its duplicate live-Ollama tests folded into `test_live_ollama.py`. Known-broken paths deliberately left untested and unchanged: `reset_password` / `ResetUserPassword` reference an unimported `requests` plus undefined locals, and `DocumentDetailView.get` references an undefined `workspaces` on its success path. Worth a follow-up ticket. ## Test plan - [x] `cd llm_be && uv run python manage.py test` → 248 tests, OK (6 skipped, all opt-in live) - [x] Same suite against Postgres 16 (`DATABASE_URL=postgres://…`) → OK, matching the containerized run in `deploy.yml` - [x] `uv run black` clean on all added files - [ ] Gitea Actions **Unit Tests** + **CI** green on this PRReviewed-on: #12
This commit was merged in pull request #12.
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
import os
|
||||
from unittest import TestCase, mock
|
||||
from unittest.mock import MagicMock, patch, AsyncMock
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from django.test import TestCase as DjangoTestCase
|
||||
|
||||
from chat_backend.services.rag_services import (
|
||||
RAGService,
|
||||
SyncRAGService,
|
||||
AsyncRAGService,
|
||||
)
|
||||
from chat_backend.models import Conversation, Prompt, DocumentWorkspace, Document
|
||||
from chat_backend.services.prompt_classifier.prompt_classifier import PromptClassifier, PromptType
|
||||
from parameterized import parameterized
|
||||
|
||||
|
||||
|
||||
class PromptClassifierTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.service = PromptClassifier()
|
||||
|
||||
@parameterized.expand([
|
||||
["Tell me a joke",PromptType.GENERAL_CHAT],
|
||||
["Create an image of a dog for me",PromptType.IMAGE_GENERATION],
|
||||
["highlight the features of the backyard playset if they were to choose us and make the language more long form",PromptType.GENERAL_CHAT],
|
||||
["Great, can you make it about a duck now", PromptType.IMAGE_GENERATION],
|
||||
])
|
||||
def test_prompt_classification(self, prompt, expected_output):
|
||||
result = self.service.classify(prompt)
|
||||
self.assertEqual(result, expected_output)
|
||||
@@ -1,32 +0,0 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest import TestCase
|
||||
|
||||
from chat_backend.services.prompt_classifier.prompt_classifier import (
|
||||
PromptClassifier,
|
||||
PromptType,
|
||||
)
|
||||
from parameterized import parameterized
|
||||
|
||||
|
||||
@unittest.skipIf(
|
||||
os.environ.get("SKIP_RAG_INIT", "").lower() in {"1", "true", "yes"},
|
||||
"Requires live Ollama; skipped when SKIP_RAG_INIT is set",
|
||||
)
|
||||
class PromptClassifierTestCase(TestCase):
|
||||
def setUp(self):
|
||||
self.service = PromptClassifier()
|
||||
|
||||
@parameterized.expand(
|
||||
[
|
||||
["Tell me a joke", PromptType.GENERAL_CHAT],
|
||||
["Create an image of a dog for me", PromptType.IMAGE_GENERATION],
|
||||
[
|
||||
"highlight the features of the backyard playset if they were to choose us and make the language more long form",
|
||||
PromptType.GENERAL_CHAT,
|
||||
],
|
||||
]
|
||||
)
|
||||
def test_prompt_classification(self, prompt, expected_output):
|
||||
result = self.service.classify(prompt)
|
||||
self.assertEqual(result, expected_output)
|
||||
Reference in New Issue
Block a user