diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml index 8c597d6..63aa8d8 100644 --- a/.gitea/workflows/docker-build.yaml +++ b/.gitea/workflows/docker-build.yaml @@ -20,11 +20,33 @@ jobs: run: | pip install -r requirements.txt pytest - name: Run unit tests - run: pytest tests/ -v + run: pytest tests/ -v --ignore=tests/test_integration.py + + integration-test: + runs-on: ubuntu-latest + services: + qdrant: + image: docker.io/qdrant/qdrant:latest + ports: + - 6333:6333 + - 6334:6334 + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install dependencies + run: | + pip install -r requirements.txt pytest + - name: Run integration tests + run: pytest tests/test_integration.py -v + env: + QDRANT_URL: http://localhost:6333 + COLLECTION_NAME: test_mcp_maildir build: runs-on: ubuntu-latest - needs: test + needs: [test, integration-test] steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 @@ -44,9 +66,6 @@ jobs: with: images: jcabillot/mcp-maildir tags: | - #type=ref,event=branch - #type=ref,event=pr - #type=sha type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - name: Build and push diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 0000000..43bb19a --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,175 @@ +"""Integration tests for mcp-maildir with a real Qdrant instance.""" + +import os +import uuid +import pytest + +os.environ["QDRANT_URL"] = "http://localhost:6333" +os.environ["COLLECTION_NAME"] = "test_mcp_maildir" +os.environ["EMBEDDING_MODEL_NAME"] = "BAAI/bge-small-en-v1.5" + +from server import get_qdrant_client, get_embedding_model, search_emails, read_email +from qdrant_client.http import models + + +TEST_EMAILS = [ + { + "message_id": "", + "date": "2026-01-15T10:00:00", + "sender": "alice@example.com", + "sender_raw": "Alice ", + "receiver": "bob@example.com", + "receiver_raw": "Bob ", + "subject": "Hello World", + "body_text": "This is a test email about Python programming and vector databases.", + "attachments": [], + }, + { + "message_id": "", + "date": "2026-01-16T14:30:00", + "sender": "carol@other.com", + "sender_raw": "Carol ", + "receiver": "alice@example.com", + "receiver_raw": "Alice ", + "subject": "Qdrant setup help", + "body_text": "Can you help me set up Qdrant for semantic email search?", + "attachments": ["screenshot.png"], + }, + { + "message_id": "", + "date": "2026-02-01T09:00:00", + "sender": "bob@example.com", + "sender_raw": "Bob ", + "receiver": "alice@example.com", + "receiver_raw": "Alice ", + "subject": "Meeting next week", + "body_text": "Let's discuss the project roadmap and data pipeline.", + "attachments": [], + }, +] + + +@pytest.fixture(scope="module") +def qdrant_setup(): + """Create collection and index test emails once per test run.""" + client = get_qdrant_client() + model = get_embedding_model() + collection_name = os.environ["COLLECTION_NAME"] + + # Clean up from previous runs + try: + client.delete_collection(collection_name) + except Exception: + pass + + # Create collection + probe = next(iter(model.embed(["dimension_probe"]))) + client.create_collection( + collection_name=collection_name, + vectors_config=models.VectorParams( + size=len(probe), distance=models.Distance.COSINE + ), + ) + + # Index test emails + for email in TEST_EMAILS: + vector_text = ( + f"Date: {email['date']}\n" + f"From: {email['sender']}\n" + f"To: {email['receiver']}\n" + f"Subject: {email['subject']}\n\n" + f"{email['body_text']}\n\n" + f"Attachments: {', '.join(email['attachments']) if email['attachments'] else 'None'}" + ) + vector = list(model.embed([vector_text]))[0].tolist() + + # Create payload indexes on first upsert + client.create_payload_index( + collection_name=collection_name, + field_name="sender", + field_schema=models.PayloadSchemaType.KEYWORD, + ) + client.create_payload_index( + collection_name=collection_name, + field_name="receiver", + field_schema=models.PayloadSchemaType.KEYWORD, + ) + client.create_payload_index( + collection_name=collection_name, + field_name="date", + field_schema=models.PayloadSchemaType.DATETIME, + ) + + client.upsert( + collection_name=collection_name, + points=[ + models.PointStruct( + id=str(uuid.uuid5(uuid.NAMESPACE_OID, email["message_id"])), + vector=vector, + payload={ + "message_id": email["message_id"], + "date": email["date"], + "sender": email["sender"], + "sender_raw": email["sender_raw"], + "receiver": email["receiver"], + "receiver_raw": email["receiver_raw"], + "subject": email["subject"], + "body_text": email["body_text"], + "attachments": email["attachments"], + }, + ) + ], + ) + + yield client + + # Cleanup + try: + client.delete_collection(collection_name) + except Exception: + pass + + +class TestSearchEmails: + def test_search_by_content(self, qdrant_setup): + """Search for emails about Python/programming.""" + result = search_emails(query="Python programming") + assert result["count"] >= 1 + assert "results" in result + assert any("Python" in r.get("body_text", "") for r in result["results"]) + + def test_search_with_participant_filter(self, qdrant_setup): + """Search emails sent by alice.""" + result = search_emails(query="help", participant="alice@example.com") + assert result["count"] >= 1 + messages = [r["message_id"] for r in result["results"]] + assert "" in messages + + def test_search_with_date_filter(self, qdrant_setup): + """Search emails after a specific date.""" + result = search_emails(query="meeting", start_date="2026-02-01") + assert result["count"] >= 1 + for r in result["results"]: + date = r.get("date", "") + assert date >= "2026-02-01" + + def test_search_no_results(self, qdrant_setup): + """Search for something that doesn't exist.""" + result = search_emails(query="gobbledygookxyz123") + assert result["count"] == 0 + assert result["results"] == [] + + +class TestReadEmail: + def test_read_existing_email(self, qdrant_setup): + """Read an email by its message_id.""" + result = read_email(message_id="") + assert "error" not in result + assert result["message_id"] == "" + assert "This is a test email" in result.get("body_text", "") + + def test_read_nonexistent_email(self, qdrant_setup): + """Read an email that doesn't exist.""" + result = read_email(message_id="") + assert "error" in result + assert "No email found" in result["error"]